Luigit
repositories / will

will

owned by admin

web/tests/phenotype.spec.ts

Raw
import { 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()
})