Files
BoosAPI/desktop/libs/stream-monaco/scripts/playwright-diff-smoke.mjs
T
boos e0a5f57793 feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as
a monorepo subdirectory under desktop/. Source only — node_modules,
dist, Cargo target and the upstream .git history are excluded.

desktop/ keeps its own independent Cargo workspace (desktop/src-tauri
with crates/*); it is not part of the backend boosapi crate, so a
`cargo check` at the metapi root still compiles only the backend. Build
the desktop app from desktop/src-tauri per desktop/README.md.

Note: app icons are still placeholder assets pending the final BoosAPI
logo. The updater signing keys live outside the repo and are not
included.
2026-06-22 11:32:57 +08:00

141 lines
4.3 KiB
JavaScript

#!/usr/bin/env node
// Smoke test for DiffEditor streaming rendering.
// Starts the Vite demo, opens /diff in headless Chromium, and asserts Monaco renders text.
import net from 'node:net'
import process from 'node:process'
import { spawn } from 'node:child_process'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { chromium } from 'playwright'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const repoRoot = path.resolve(__dirname, '..')
const demoDir = path.join(repoRoot, 'examples', 'streaming-demo')
function isPortOpen(port, host = '127.0.0.1') {
return new Promise((resolve) => {
const sock = net.createConnection({ host, port })
sock.on('connect', () => {
sock.end()
resolve(true)
})
sock.on('error', () => {
sock.destroy()
resolve(false)
})
})
}
async function findFreePort(start = 5173, end = 5190) {
for (let port = start; port <= end; port++) {
// If nothing is listening, consider it free.
// (We only bind after Vite starts, so this is best-effort.)
// eslint-disable-next-line no-await-in-loop
const open = await isPortOpen(port)
if (!open)
return port
}
throw new Error(`No free port found in ${start}-${end}`)
}
async function waitForPort(port, ms = 20000) {
const start = Date.now()
// eslint-disable-next-line no-constant-condition
while (true) {
// eslint-disable-next-line no-await-in-loop
const open = await isPortOpen(port)
if (open)
return
if (Date.now() - start > ms)
throw new Error(`Timed out waiting for port ${port}`)
// eslint-disable-next-line no-await-in-loop
await new Promise(r => setTimeout(r, 150))
}
}
function killProcessTree(child) {
if (!child || child.killed)
return
try {
child.kill('SIGTERM')
}
catch { }
// Fallback hard kill after a short grace period.
setTimeout(() => {
try {
if (!child.killed)
child.kill('SIGKILL')
}
catch { }
}, 3000).unref?.()
}
async function run() {
const port = process.env.PORT ? Number(process.env.PORT) : await findFreePort()
if (!Number.isFinite(port))
throw new Error(`Invalid PORT: ${process.env.PORT}`)
const vite = spawn(
'pnpm',
['-C', demoDir, 'dev', '--host', '127.0.0.1', '--port', String(port), '--strictPort'],
{ stdio: ['ignore', 'pipe', 'pipe'], env: process.env },
)
const logs = []
vite.stdout.on('data', d => logs.push(String(d)))
vite.stderr.on('data', d => logs.push(String(d)))
const onExit = () => killProcessTree(vite)
process.on('SIGINT', onExit)
process.on('SIGTERM', onExit)
process.on('exit', onExit)
try {
await waitForPort(port)
const url = `http://127.0.0.1:${port}/diff`
const browser = await chromium.launch({ headless: true })
const page = await browser.newPage()
await page.goto(url, { waitUntil: 'networkidle' })
await page.waitForSelector('.monaco-diff-editor', { timeout: 20000 })
await page.waitForTimeout(1200)
const before = await page.evaluate(() => {
const viewLines = Array.from(document.querySelectorAll('.monaco-editor .view-lines'))
const text = viewLines.map(v => (v.textContent ?? '')).join('\n').trim()
return { viewLinesCount: viewLines.length, textLen: text.length, sample: text.slice(0, 120) }
})
// Wait for at least one streaming tick from the demo.
await page.waitForTimeout(700)
const after = await page.evaluate(() => {
const viewLines = Array.from(document.querySelectorAll('.monaco-editor .view-lines'))
const text = viewLines.map(v => (v.textContent ?? '')).join('\n').trim()
return { viewLinesCount: viewLines.length, textLen: text.length }
})
await browser.close()
// We only assert that Monaco renders *some* text and stays rendered after
// streaming ticks. Diff line wrapping/reflow can change `textLen`, so do
// not assert monotonic growth.
const ok = before.textLen > 0 && after.textLen > 0
const result = { ok, url, before, after }
console.log(JSON.stringify(result, null, 2))
if (!ok) {
console.error('Diff smoke failed; recent Vite logs:\n', logs.slice(-40).join(''))
process.exit(1)
}
}
finally {
killProcessTree(vite)
}
}
run().catch((err) => {
console.error(err)
process.exit(1)
})