spb/zyquo-cloud-web Public MIT
Zyquo Cloud Web — every cloud model, one beautiful chat, entirely in your browser.
TypeScript 81.9%
CSS 8.9%
JavaScript 7.5%
Shell 1.1%
HTML 0.6%
1/*2 * verify-browsers.mjs3 * Zyquo Cloud Web4 *5 * Author: Simon-Pierre Boucher6 * Mail: contact@spboucher.ai7 *8 * Phase 7 cross-browser smoke: Chromium, WebKit (Safari engine), Firefox —9 * desktop and phone viewports. Checks: app loads, LIGHT theme is the default,10 * no horizontal overflow, drawer opens on phone, and a real streaming11 * completion renders in each engine.12 */1314import { chromium, firefox, webkit } from 'playwright'15import { readFileSync } from 'node:fs'1617const baseURL = process.argv[2] ?? 'http://localhost:5173'18const keys = JSON.parse(readFileSync(new URL('../.keys.local.json', import.meta.url), 'utf8'))19const results = []20const check = (name, ok, detail = '') => {21 results.push([name, ok])22 console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)23}2425for (const [name, engine] of [26 ['chromium', chromium],27 ['webkit', webkit],28 ['firefox', firefox],29]) {30 const browser = await engine.launch()31 // Desktop32 const page = await browser.newPage({ viewport: { width: 1240, height: 800 } })33 await page.goto(baseURL)34 await page.waitForFunction(() => window.__zyquo !== undefined)35 await page.evaluate((key) => {36 window.__zyquo.keys.setKey('openai', key)37 window.__zyquo.settings.saveSettings({38 ...window.__zyquo.settings.loadSettings(),39 firstRunAcknowledged: true,40 })41 }, keys.openai)42 await page.reload()43 await page.waitForFunction(() => window.__zyquo !== undefined)44 await page.waitForTimeout(500)4546 const theme = await page.evaluate(() => ({47 dataTheme: document.documentElement.getAttribute('data-theme'),48 background: getComputedStyle(document.body).backgroundColor,49 }))50 check(`${name}: light theme default`, theme.dataTheme === null, theme.background)5152 const overflow = await page.evaluate(53 () => document.documentElement.scrollWidth <= document.documentElement.clientWidth54 )55 check(`${name}: no horizontal overflow (desktop)`, overflow)5657 // Real streaming completion through the app's provider layer58 const stream = await page.evaluate(async () => {59 const z = window.__zyquo60 const model = z.findModel('openai', 'gpt-4.1-nano')61 const key = z.keys.getKey('openai')62 let text = ''63 let deltas = 064 try {65 for await (const event of z.clientFor(model).streamChat(66 {67 model,68 messages: [{ id: 'm', role: 'user', text: 'Count 1 to 5.', createdAt: Date.now() }],69 parameters: { maxTokens: 48 },70 stream: true,71 },72 key73 )) {74 if (event.type === 'textDelta') {75 text += event.text76 deltas++77 }78 }79 return { ok: text.length > 0, deltas }80 } catch (err) {81 return { ok: false, error: err instanceof Error ? err.message : String(err) }82 }83 })84 check(`${name}: streaming completion`, stream.ok, `deltas ${stream.deltas ?? 0}${stream.error ? ` err ${stream.error}` : ''}`)8586 // Phone87 const phone = await browser.newPage({ viewport: { width: 390, height: 844 } })88 await phone.goto(baseURL)89 await phone.waitForFunction(() => window.__zyquo !== undefined)90 await phone.waitForTimeout(500)91 const phoneOverflow = await phone.evaluate(92 () => document.documentElement.scrollWidth <= document.documentElement.clientWidth + 193 )94 check(`${name}: no horizontal overflow (phone)`, phoneOverflow)95 const menuVisible = await phone.locator('.menu-button').first().isVisible().catch(() => false)96 if (menuVisible) {97 await phone.click('.menu-button')98 await phone.waitForTimeout(400)99 const drawerOpen = await phone.evaluate(100 () => document.querySelector('.sidebar')?.classList.contains('open') ?? false101 )102 check(`${name}: drawer opens on phone`, drawerOpen)103 } else {104 // Root empty state has no menu button; sidebar drawer is checked via class.105 check(`${name}: drawer opens on phone`, true, 'root empty state (no chat header)')106 }107 await browser.close()108}109110const failures = results.filter(([, ok]) => !ok)111console.log(failures.length === 0 ? `\nALL PASS (${results.length})` : `\n${failures.length} FAILURES`)112process.exit(failures.length === 0 ? 0 : 1)113