90 lines
6.7 KiB
TypeScript
90 lines
6.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 mara = page.locator('.evidence-card.party').filter({ has: page.getByRole('heading', { name: 'Mara Voss', exact: true }) })
|
||
const movement = page.locator('.evidence-card.folder').filter({ hasText: 'MOVEMENT RECORDS' })
|
||
await mara.click()
|
||
await page.getByRole('button', { name: 'CONNECT', exact: true }).click()
|
||
await waitForSave(page, () => movement.click())
|
||
|
||
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 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)
|
||
})
|