107 lines
7.1 KiB
JavaScript
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 })
|