// cinny-desktop #19: Windows smoke test against the INSTALLED app. // // The app is started with WebView2's DevTools port open // (WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=9222) and this // script drives its real page over CDP with playwright-core. No login: every // check runs on the login screen or through the app's own Tauri commands. // // node scripts/windows-smoke.mjs // // Checks whose feature isn't in the build under test are reported "n/a" // instead of failing, so the same script runs on main and on PR branches. import { writeFileSync, mkdirSync } from 'node:fs'; import { execSync } from 'node:child_process'; import { chromium } from 'playwright-core'; const OUT = process.argv[2] || 'smoke-out'; mkdirSync(OUT, { recursive: true }); const APP = 'http://localhost:44548'; const results = []; const record = (name, status, detail = '') => { results.push({ name, status, detail }); console.log(`${status.toUpperCase().padEnd(4)} ${name}${detail ? ` — ${detail}` : ''}`); }; const until = async (fn, ms, step = 500) => { const end = Date.now() + ms; for (;;) { let v; try { v = await fn(); } catch { v = undefined; } if (v || Date.now() > end) return v; await new Promise((r) => setTimeout(r, step)); } }; // 1. Connect to the running app's WebView2. const browser = await until(() => chromium.connectOverCDP('http://127.0.0.1:9222'), 60_000, 1000); if (!browser) { record('connect to the app over CDP', 'fail', 'no DevTools endpoint on :9222'); writeFileSync(`${OUT}/results.json`, JSON.stringify(results, null, 2)); process.exit(1); } const page = await until( () => browser.contexts().flatMap((c) => c.pages()).find((p) => p.url().startsWith(APP)), 60_000, ); if (!page) { record('app page found', 'fail', browser.contexts().flatMap((c) => c.pages()).map((p) => p.url()).join(', ')); process.exit(1); } record('app page found', 'pass', page.url()); // 2. Boots to the login screen. const booted = await until(async () => /Login|Homeserver/.test(await page.locator('body').innerText()), 60_000); record('boots to the login screen', booted ? 'pass' : 'fail'); await page.screenshot({ path: `${OUT}/01-login.png` }); // 3. The local server's address (cinny #43 binds 127.0.0.1 explicitly). try { const listen = execSync( 'powershell -NoProfile -Command "(Get-NetTCPConnection -LocalPort 44548 -State Listen).LocalAddress -join \',\'"', ) .toString() .trim(); record('local server listening', listen ? 'pass' : 'fail', listen); } catch (e) { record('local server listening', 'fail', String(e).slice(0, 120)); } const cfg = await page.evaluate(() => fetch('/config.json').then((r) => r.json())); // 4. Microphone for the app itself (WebView2 PermissionRequested handler, #22). const appMic = await page.evaluate(async () => { try { const s = await navigator.mediaDevices.getUserMedia({ audio: true }); s.getTracks().forEach((t) => t.stop()); return 'ok'; } catch (e) { return `${e.name}: ${e.message}`; } }); record('microphone allowed for the app', appMic === 'ok' ? 'pass' : 'fail', appMic); // 5. Call page on its own origin, isolated from the app (cinny #43 / #27). if (cfg.desktopCallOrigin) { const src = `${cfg.desktopCallOrigin}/public/element-call/index.html`; await page.evaluate((s) => { const f = document.createElement('iframe'); f.id = 'smoke-call'; f.src = s; f.allow = 'microphone; camera; display-capture; autoplay; clipboard-write;'; f.sandbox = 'allow-forms allow-scripts allow-same-origin allow-popups allow-modals allow-downloads'; document.body.appendChild(f); }, src); const frame = await until(() => page.frames().find((f) => f.url().startsWith(src)), 30_000); if (!frame) { record('call page loads from its own origin', 'fail', 'frame did not load (CSP?)'); } else { await frame.waitForLoadState('domcontentloaded').catch(() => undefined); const iso = await frame.evaluate(async () => { const r = { origin: location.origin }; try { r.parentStorage = String(parent.localStorage.length); } catch (e) { r.parentStorage = e.name; } try { const s = await navigator.mediaDevices.getUserMedia({ audio: true }); s.getTracks().forEach((t) => t.stop()); r.mic = 'ok'; } catch (e) { r.mic = e.name; } return r; }); record('call page loads from its own origin', 'pass', iso.origin); record('call page cannot read the app storage', iso.parentStorage === 'SecurityError' ? 'pass' : 'fail', iso.parentStorage); record('call page gets the microphone', iso.mic === 'ok' ? 'pass' : 'fail', iso.mic); } } else { record('call page on its own origin', 'n/a', 'desktopCallOrigin not set in this build'); } // 6. OS keychain round trip (cinny #105, step 1). const kc = await page.evaluate(async () => { const inv = window.__TAURI_INTERNALS__?.invoke; if (!inv) return { error: 'no Tauri bridge' }; try { const supported = await inv('secure_session_supported'); if (!supported) return { supported }; const tokens = { userId: '@smoke:ci.invalid', deviceId: 'SMOKE', accessToken: `smoke-${Date.now()}` }; const before = await inv('secure_session_get'); await inv('secure_session_set', { tokens }); const back = await inv('secure_session_get'); // Put back whatever was there (nothing, on a clean runner). if (before) await inv('secure_session_set', { tokens: before }); else await inv('secure_session_clear'); const after = await inv('secure_session_get'); return { supported, roundTrip: back?.accessToken === tokens.accessToken, restored: JSON.stringify(after) === JSON.stringify(before ?? null) }; } catch (e) { return { error: String(e).slice(0, 160) }; } }); if (kc.error && /not found/i.test(kc.error)) record('keychain round trip', 'n/a', 'commands not in this build'); else if (kc.error) record('keychain round trip', 'fail', kc.error); else if (!kc.supported) record('keychain round trip', 'fail', 'secure_session_supported = false on Windows'); else record('keychain round trip', kc.roundTrip && kc.restored ? 'pass' : 'fail', JSON.stringify(kc)); // 7. A foreign page loaded in the window must not get the microphone (#22). await page.goto('https://example.com/').catch(() => undefined); const foreignMic = await page.evaluate(async () => { try { const s = await navigator.mediaDevices.getUserMedia({ audio: true }); s.getTracks().forEach((t) => t.stop()); return 'ok'; } catch (e) { return e.name; } }); record( 'microphone refused to a foreign page', foreignMic === 'NotAllowedError' ? 'pass' : 'info', `${foreignMic}${foreignMic === 'ok' ? ' (build without the #22 origin check)' : ''}`, ); await page.goto(APP).catch(() => undefined); await page.screenshot({ path: `${OUT}/02-end.png` }).catch(() => undefined); writeFileSync(`${OUT}/results.json`, JSON.stringify(results, null, 2)); await browser.close().catch(() => undefined); const failed = results.filter((r) => r.status === 'fail'); console.log(`\n${results.filter((r) => r.status === 'pass').length} passed, ${failed.length} failed`); process.exit(failed.length ? 1 : 0);