#!/usr/bin/env node // Money-free, synthetic browser check of a disposable local container only. // This is not real signer, relay, node-lifecycle or physical Companion evidence. const { createRequire } = require('node:module') const { resolve } = require('node:path') const assert = require('node:assert/strict') const http = require('node:http') const load = createRequire(resolve(__dirname, '../neode-ui/package.json')) const { chromium } = load('playwright') const base = new URL(process.env.KITHMOOT_TEST_URL || 'http://127.0.0.1:8189') if (base.hostname !== '127.0.0.1' || base.protocol !== 'http:' || base.username || base.password) throw new Error('Use a disposable loopback HTTP endpoint only') ;(async () => { const browser = await chromium.launch({ headless: true, chromiumSandbox: true, args: ['--use-fake-ui-for-media-stream', '--use-fake-device-for-media-stream'] }) try { for (const width of [390, 1440]) { const context = await browser.newContext({ viewport: { width, height: 900 }, serviceWorkers: 'block' }) await context.route('**/*', route => new URL(route.request().url()).origin === base.origin ? route.continue() : route.abort()) await context.routeWebSocket('**/*', socket => socket.close()) // Never use an installed identity. Exercise cancellation with a synthetic // provider before the packaged canonical provider sees the document. await context.addInitScript(() => { window.__archipelagoNostr = true window.__pickerCalls = 0 window.archipelagoNostr = { selectIdentity: async () => { window.__pickerCalls++; throw new Error('Synthetic user cancellation') }, getSelectedIdentity: () => null, } }) const page = await context.newPage() const errors = [] page.on('pageerror', error => errors.push(error.message)) await page.goto(new URL('/j/', base).href) await page.waitForFunction(() => document.documentElement.dataset.ordinaryStartup === 'ready', { timeout: 30000 }) await page.waitForFunction(() => window.__pickerCalls === 1) await page.waitForTimeout(500) assert.equal(await page.evaluate(() => window.__pickerCalls), 1, 'cancellation must not reopen the picker') assert.equal(await page.title(), 'KithMoot') assert.equal(await page.locator('script[src*="nostr-provider"]').count(), 1) assert.equal(await page.getByRole('button', { name: 'Use another signer' }).count(), 1) assert.equal(await page.locator('#archipelagoPackageNotice').isVisible(), true) const overflow = await page.evaluate(() => document.documentElement.scrollWidth > innerWidth + 1) assert.equal(overflow, false, `horizontal overflow at ${width}px`) await page.evaluate(() => localStorage.setItem('kithmoot.name', 'Synthetic Preview')) await page.reload() await page.waitForFunction(() => document.documentElement.dataset.ordinaryStartup === 'ready') assert.equal(await page.evaluate(() => localStorage.getItem('kithmoot.name')), 'Synthetic Preview') assert.deepEqual(errors, [], 'no uncaught browser errors') console.log(`PASS packaged UI ${width}px: startup, one cancelled chooser, fallback signer, provider, layout and reload persistence`) await context.close() } // Cross-origin iframe fixture: no real microphone, camera, account or relay. const host = http.createServer((request, response) => { const allow = request.url === '/denied' ? '' : `camera 'src'; microphone 'src'; display-capture 'src'; picture-in-picture 'src'; fullscreen 'src'` response.setHeader('Content-Type', 'text/html') response.end(``) }) await new Promise(resolve => host.listen(0, '127.0.0.1', resolve)) const hostOrigin = `http://127.0.0.1:${host.address().port}` try { for (const permitted of [false, true]) { const context = await browser.newContext({ serviceWorkers: 'block', viewport: { width: 1440, height: 1000 } }) await context.route('**/*', route => [base.origin, hostOrigin].includes(new URL(route.request().url()).origin) ? route.continue() : route.abort()) await context.routeWebSocket('**/*', socket => socket.close()) await context.addInitScript(() => { window.__archipelagoNostr = true }) const page = await context.newPage() await page.goto(hostOrigin + (permitted ? '/allowed' : '/denied')) const frame = page.frames().find(frame => frame.url().startsWith(base.origin)) assert.ok(frame) await frame.waitForFunction(() => document.documentElement.dataset.ordinaryStartup === 'ready') const captured = await frame.evaluate(async () => { try { window.__fixtureStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }) const video = document.createElement('video') video.dataset.track = 'synthetic-fixture'; video.muted = true; video.autoplay = true; video.playsInline = true video.srcObject = window.__fixtureStream; document.body.append(video); await video.play() window.__fixtureVideo = video return true } catch { return false } }) assert.equal(captured, permitted, 'iframe capture must require explicit delegation') if (permitted) { await frame.waitForFunction(() => window.__fixtureVideo.videoWidth > 0) await frame.getByRole('button', { name: 'Picture-in-picture', exact: true }).click() await frame.waitForFunction(() => document.pictureInPictureElement === window.__fixtureVideo) await frame.getByRole('button', { name: 'Picture-in-picture', exact: true }).click() await frame.waitForFunction(() => !document.pictureInPictureElement) assert.equal(await frame.evaluate(() => window.__fixtureVideo.srcObject === window.__fixtureStream && window.__fixtureStream.getTracks().every(track => track.readyState === 'live')), true) await frame.evaluate(() => window.__fixtureStream.getTracks().forEach(track => track.stop())) console.log('PASS cross-origin iframe: synthetic capture, real browser PiP enter/return, same live stream') } else console.log('PASS cross-origin iframe: capture denied without delegation') await context.close() } } finally { await new Promise(resolve => host.close(resolve)) } const response = await fetch(new URL('/nostr-provider.js', base)) assert.equal(response.status, 200) assert.match(response.headers.get('cache-control'), /no-store/) for (const path of ['/healthz', '/j/video-entry.mjs', '/j/archipelago-video.mjs']) { assert.equal((await fetch(new URL(path, base))).status, 200, path) } assert.equal((await fetch(new URL('/turn', base))).status, 404) assert.equal((await fetch(new URL('/codes', base))).status, 404) console.log('PASS HTTP package: provider cache policy, health, PiP modules and truthful missing services') } finally { await browser.close() } })().catch(error => { console.error(error); process.exitCode = 1 })