Files
gupi-osint-board/e2e/mystery.acceptance.spec.ts
T

118 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { expect, test, type Page } from '@playwright/test'
async function waitForSave(page: Page, action: () => Promise<void>) {
const response = page.waitForResponse(candidate => candidate.request().method() === 'PUT' && candidate.url().includes('/api/levels/') && candidate.ok())
await action()
await response
}
async function classify(page: Page, name: string, kind: 'PERSON' | 'ORGANIZATION', summary: string) {
if (!await page.locator('.brief-panel').isVisible()) await page.getByRole('button', { name: 'Case brief', exact: true }).click()
const concept = page.locator('.brief-concepts section').filter({ hasText: name })
await waitForSave(page, () => concept.getByRole('button', { name: kind, exact: true }).click())
await expect(page.locator('.brief-panel')).toBeVisible()
await expect(concept).toHaveClass(/\bjust-resolved\b/)
await concept.getByRole('button', { name: 'EDIT DOSSIER', exact: true }).click()
await page.getByLabel('Party summary').fill(summary)
await waitForSave(page, () => page.getByRole('button', { name: 'SAVE DOSSIER', exact: true }).click())
}
async function createEvent(page: Page, title: string, narrative: string, occurredAt: string, sources: string[]) {
await page.getByRole('button', { name: 'NEW EVENT', exact: true }).click()
await page.getByLabel('Event title').fill(title)
await page.getByLabel('Event narrative').fill(narrative)
await page.getByLabel('Occurred at').fill(occurredAt)
for (const source of sources) {
await page.locator('.event-support-list .folder-member').filter({ hasText: source }).locator('input[type="checkbox"]').check()
}
await waitForSave(page, () => page.getByRole('button', { name: 'SAVE EVENT', exact: true }).click())
}
test('a cloned Glass Harbor level can be solved without modifying its template', async ({ page, request }) => {
const levels = await (await request.get('/api/levels')).json() as { id: string }[]
const playable = levels.find(level => level.id.startsWith('glass-harbor-case-'))
if (!playable) throw new Error('Glass Harbor playable clone was not seeded')
await page.goto(`/?level=${playable.id}`)
await expect(page.getByRole('heading', { name: 'The Glass Harbor Diversion' })).toBeVisible()
await expect(page.locator('.evidence-card.folder')).toHaveCount(3)
await expect(page.locator('.timeline .marker')).toHaveCount(8)
await expect(page.locator('.timeline-label button')).toHaveText('1987-10-01 — 1987-10-31')
const dispatchMarker = await page.getByRole('button', { name: '1987-10-16 — Carrier Dispatch Manifest · H&F 14', exact: true }).boundingBox()
const auctionMarker = await page.getByRole('button', { name: '1987-10-24 — Meridian Maritime Auction · Lot 117', exact: true }).boundingBox()
expect(dispatchMarker).not.toBeNull()
expect(auctionMarker).not.toBeNull()
expect(auctionMarker!.x - dispatchMarker!.x).toBeGreaterThan(100)
await page.getByRole('button', { name: 'TIMELINE', exact: true }).click()
await page.getByLabel('Timeline start date').fill('1987-10-10')
await page.getByLabel('Timeline end date').fill('1987-10-26')
await waitForSave(page, () => page.getByRole('button', { name: 'APPLY RANGE', exact: true }).click())
await expect(page.locator('.timeline-label button')).toHaveText('1987-10-10 — 1987-10-26')
await classify(page, 'Mara Voss', 'PERSON', 'Restoration project officer who sponsored the Warehouse 3 access pass.')
await classify(page, 'Elias Vale', 'PERSON', 'Driver of H&F 14; transported CO-771 to Warehouse 3.')
await classify(page, 'Greyhaven Preservation Society', 'ORGANIZATION', 'Commissioned the North Quay restoration and owned the missing lens.')
await classify(page, 'Calder Optical Works', 'ORGANIZATION', 'Supplier that released sealed crate CO-771 to the approved carrier.')
await classify(page, 'Harbor & Fell Logistics', 'ORGANIZATION', 'Approved carrier whose vehicle delivered the crate to the wrong address.')
await classify(page, 'Voss Antiquities Ltd', 'ORGANIZATION', 'Mara Vosss company, based at Warehouse 3 and named as auction consignor.')
await page.getByRole('button', { name: 'RETURN TO BOARD', exact: true }).click()
await createEvent(page, 'CO-771 left Calder for North Quay', 'Calder released the sealed lens to Elias Vale in H&F 14 with no alternate delivery authority.', '1987-10-16T16:40', ['Carrier Dispatch Manifest'])
await createEvent(page, 'CO-771 was diverted to Warehouse 3', 'Mara Voss sponsored access; H&F 14 entered loaded and left empty. The next-day memorandum was retrospective.', '1987-10-17T22:08', ['Old Glass Harbor Gate Ledger', 'Warehouse 3 Security Photograph', 'Delivery Redirection Memorandum'])
await createEvent(page, 'Voss Antiquities offered the lens for sale', 'The auction description and seller reference identify the missing assembly and the company that stood to profit.', '1987-10-24T12:00', ['Company Register Extract', 'Meridian Maritime Auction'])
page.once('dialog', dialog => dialog.accept('The redirection memorandum was written after the crate had already moved.'))
await waitForSave(page, () => page.getByRole('button', { name: 'NEW NOTE', exact: true }).click())
const elias = page.locator('.evidence-card.party').filter({ has: page.getByRole('heading', { name: 'Elias Vale', exact: true }) })
const movement = page.locator('.evidence-card.folder').filter({ hasText: 'MOVEMENT RECORDS' })
await waitForSave(page, () => page.getByRole('button', { name: 'Zoom out', exact: true }).click())
await waitForSave(page, () => movement.getByRole('button', { name: 'OPEN', exact: true }).click())
const manifest = page.locator('.source-file-widget.open').filter({ hasText: 'Carrier Dispatch Manifest' })
await elias.click()
await page.getByRole('button', { name: 'Red thread', exact: true }).click()
const boardBox = await page.locator('.board-viewport').boundingBox()
if (!boardBox) throw new Error('Board is not visible')
await page.mouse.move(boardBox.x + boardBox.width / 2, boardBox.y + boardBox.height / 2)
await expect(page.locator('.connections .thread-preview path')).toBeVisible()
const manifestBox = await manifest.boundingBox()
if (!manifestBox) throw new Error('Carrier Dispatch Manifest is not visible')
await page.mouse.move(manifestBox.x + manifestBox.width / 2, manifestBox.y + manifestBox.height / 2)
await manifest.click()
const threadEditor = page.locator('.thread-editor')
await expect(threadEditor.getByText('Elias Vale', { exact: true })).toBeVisible()
await expect(threadEditor.getByText('Carrier Dispatch Manifest · H&F 14', { exact: true })).toBeVisible()
await page.getByLabel('Thread tag').fill('Proof Elias is the driver')
await page.getByRole('slider', { name: 'Thread tightness', exact: true }).fill('85')
await waitForSave(page, () => page.getByRole('button', { name: 'ADD TAG & TIGHTEN', exact: true }).click())
await expect(page.locator('.connections g.tightening path')).toBeVisible()
await page.reload()
await expect(page.locator('.evidence-card.party')).toHaveCount(6)
await expect(page.locator('.evidence-card.event')).toHaveCount(3)
await expect(page.locator('.evidence-card.note')).toHaveCount(1)
await expect(page.locator('.story-strip')).toContainText('CO-771 was diverted to Warehouse 3')
await expect(page.locator('.event-support-lines line')).toHaveCount(6)
await expect(page.locator('.connections path')).toHaveCount(1)
const relationTag = page.locator('.thread-tag')
await expect(relationTag).toContainText('Proof Elias is the driver')
await relationTag.click()
await expect(relationTag).toHaveClass(/\bexpanded\b/)
await expect(relationTag).toHaveAttribute('aria-expanded', 'true')
await relationTag.click()
await expect(page.getByText('Edit red thread')).toBeVisible()
await page.getByRole('button', { name: 'Close thread editor', exact: true }).click()
const solved = await (await request.get(`/api/levels/${playable.id}`)).json()
expect(solved.connections).toContainEqual(expect.objectContaining({ label: 'Proof Elias is the driver', tightness: 85 }))
const template = await (await request.get('/api/templates')).json() as { slug: string; currentVersion: number }[]
expect(template).toContainEqual(expect.objectContaining({ slug: 'glass-harbor', currentVersion: 1 }))
const authoringLevels = levels.filter(level => level.id.startsWith('glass-harbor-authoring-'))
expect(authoringLevels).toHaveLength(1)
const untouched = await (await request.get(`/api/levels/${authoringLevels[0].id}?edit=1`)).json()
expect(untouched.evidence.filter((item: { type: string }) => item.type === 'party')).toHaveLength(0)
expect(untouched.evidence.filter((item: { type: string }) => item.type === 'event')).toHaveLength(0)
expect(untouched.brief.concepts.every((concept: { resolvedPartyExhibitId?: string }) => !concept.resolvedPartyExhibitId)).toBe(true)
})