repositories / will
will
owned by admin
web/tests/phenotype.spec.ts
Rawimport { test, expect } from '@playwright/test'
import { mkdtemp, mkdir } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { spawn, type ChildProcess } from 'node:child_process'
import { fileURLToPath } from 'node:url'
/**
* Browser e2e (spec AC23): timeline renders live SSE events, chat view loads,
* and the interface stays keyboard operable. Boots the real service on a
* tmpdir with the built frontend.
*/
const here = fileURLToPath(new URL('.', import.meta.url))
let service: ChildProcess | undefined
let dataDir = ''
let port = 0
test.beforeAll(async () => {
dataDir = await mkdtemp(join(tmpdir(), 'e2e-web-'))
const webDist = join(dataDir, 'web-dist')
await mkdir(webDist, { recursive: true })
// use the repo build output as static root
const repoWebDist = join(here, '..', '..', 'web', 'dist')
const repoDocsDist = join(here, '..', '..', 'docs', 'dist')
// pick a free port up front so both listeners use known ports
const { createServer } = await import('node:http')
const probe = createServer()
await new Promise<void>((res) => probe.listen(0, '127.0.0.1', () => res()))
port = (probe.address() as { port: number }).port
await new Promise<void>((res) => probe.close(() => res()))
service = spawn(
process.execPath,
['--experimental-strip-types', join(here, '..', '..', 'src', 'service', 'main.ts')],
{
env: {
...process.env,
WILL_SERVICE_DATA_DIR: dataDir,
WILL_SERVICE_PUBLIC_ADDR: `127.0.0.1:${port}`,
WILL_SERVICE_INTERNAL_ADDR: `127.0.0.1:${port + 1}`,
WILL_SERVICE_WEB_DIST: repoWebDist,
WILL_SERVICE_DOCS_DIST: repoDocsDist,
},
stdio: 'ignore',
},
)
const deadline = Date.now() + 15_000
for (;;) {
try {
const res = await fetch(`http://127.0.0.1:${port}/healthz`)
if (res.ok) break
} catch {
// not up yet
}
if (Date.now() > deadline) throw new Error('service did not become healthy')
await new Promise((r) => setTimeout(r, 250))
}
})
test.afterAll(() => {
service?.kill()
})
test('timeline renders live events via SSE', async ({ page }) => {
await page.goto(`http://127.0.0.1:${port}/`)
await expect(page.getByRole('heading', { name: 'Timeline' })).toBeVisible()
// ingest an event through the internal API and watch it arrive live
const intPort = port + 1
await fetch(`http://127.0.0.1:${intPort}/v1/audit/ingest`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
events: [{ id: crypto.randomUUID(), ts: new Date().toISOString(), kind: 'e2e_probe', payload: { hello: 'web' } }],
}),
})
await expect(page.locator('#timeline-list .event .kind', { hasText: 'e2e_probe' })).toBeVisible()
})
test('chat panel lists messages; tabs are keyboard operable', async ({ page }) => {
const intPort = port + 1
await fetch(`http://127.0.0.1:${intPort}/v1/chat/ingest`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
messages: [
{
id: `tg-${Date.now()}`,
chatId: -100,
messageId: Math.floor(Math.random() * 100000),
direction: 'in',
ts: new Date().toISOString(),
senderName: 'friend',
text: 'browser e2e message',
},
],
}),
})
await page.goto(`http://127.0.0.1:${port}/#chat`)
await expect(page.locator('.chat-msg', { hasText: 'browser e2e message' })).toBeVisible()
await page.keyboard.press('Home')
await page.locator('#tab-health').focus()
await page.keyboard.press('Enter')
await expect(page.getByRole('heading', { name: 'Health' })).toBeVisible()
await expect(page.locator('#health-facts')).toContainText('storage')
// cost view: empty storage renders the zero table, not an error
await expect(page.locator('#usage-projection')).toContainText('today')
await expect(page.locator('#usage-totals')).toContainText('total')
})
test('skip link exists and docs view loads rendered markdown', async ({ page }) => {
await page.goto(`http://127.0.0.1:${port}/#docs`)
await expect(page.getByRole('link', { name: 'Skip to content' })).toHaveCount(1)
await expect(page.locator('#docs-list a').first()).toBeVisible()
})