import { expect, test, type Page } from '@playwright/test' async function waitForSave(page: Page, action: () => Promise) { 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) { await page.getByRole('button', { name: 'BRIEF', exact: true }).click() const concept = page.locator('.brief-concepts section').filter({ hasText: name }) await concept.getByRole('button', { name: kind, 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 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) })