2026-08-14 15:02:00 +02:00
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 ) {
2026-08-14 16:18:13 +02:00
if ( ! await page . locator ( '.brief-panel' ). isVisible ()) await page . getByRole ( 'button' , { name : 'Case brief' , exact : true }). click ()
2026-08-14 15:02:00 +02:00
const concept = page . locator ( '.brief-concepts section' ). filter ({ hasText : name })
2026-08-14 16:18:13 +02:00
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 ()
2026-08-14 15:02:00 +02:00
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 )
2026-08-14 15:40:54 +02:00
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' )
2026-08-14 15:02:00 +02:00
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.' )
2026-08-14 16:18:13 +02:00
await page . getByRole ( 'button' , { name : 'RETURN TO BOARD' , exact : true }). click ()
2026-08-14 15:02:00 +02:00
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 )
})