Files
archy/scripts/test-kithmoot-package.cjs

107 lines
7.1 KiB
JavaScript

#!/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(`<iframe title="Fixture app" allow="${allow}" src="${base.origin}/j/" style="width:100%;height:850px"></iframe>`)
})
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 })