SPB Git

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%
4.1 KB · 113 lines javascript
Raw Blame History
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