118 lines
8.7 KiB
TypeScript
118 lines
8.7 KiB
TypeScript
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 Voss’s 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)
|
||
})
|