const { chromium } = require('C:/Users/phenom/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright'); const assert = require('node:assert/strict'); const crypto = require('node:crypto'); const fs = require('node:fs'); const path = require('node:path'); const directory = path.resolve('playtest-artifacts', 'dungeon-simulation'); fs.mkdirSync(directory, { recursive: true }); const databaseDirectory = path.join(directory, 'test-database'); fs.rmSync(databaseDirectory, { recursive: true, force: true }); (async () => { const { createGameServer } = await import('../server/server.mjs'); const runtime = createGameServer({ dataDir: databaseDirectory, enableShelvedModes: false, }); await new Promise((resolve) => runtime.server.listen(4189, '127.0.0.1', resolve)); const browser = await chromium.launch({ channel: 'msedge', headless: true }); const context = await browser.newContext({ viewport: { width: 1600, height: 1000 }, acceptDownloads: true }); const page = await context.newPage(); const cameraModuleUrls = fs.readdirSync('dist/assets') .filter((file) => /^(?:index|GameRuntime)-.*\.js$/.test(file)) .map((file) => `/assets/${file}`); await page.addInitScript((moduleUrls) => { window.readSpectatorCameraState = async () => { const values = (await Promise.all(moduleUrls.map((url) => import(url)))).flatMap(Object.values); const game = values.find((value) => value?.getState && 'cameraYaw' in value.getState() && 'activeDungeonId' in value.getState()); if (!game) throw new Error('Game camera store export was not found.'); return { yaw: game.getState().cameraYaw, pointerLocked: document.pointerLockElement !== null }; }; }, cameraModuleUrls); page.setDefaultTimeout(90000); const errors = []; page.on('pageerror', (error) => errors.push(error.message)); const capture = (name) => page.screenshot({ path: path.join(directory, `${name}.png`), fullPage: true }); const captureCanvas = async (name) => { const canvas = page.locator('canvas'); await canvas.evaluate((element) => { const hidden = []; let current = element; while (current.parentElement) { for (const sibling of current.parentElement.children) { if (sibling === current || !(sibling instanceof HTMLElement)) continue; hidden.push([sibling, sibling.style.visibility]); sibling.style.visibility = 'hidden'; } current = current.parentElement; } window.__restoreCanvasCapture = () => { for (const [node, visibility] of hidden) node.style.visibility = visibility; delete window.__restoreCanvasCapture; }; }); try { const image = await canvas.screenshot(name ? { path: path.join(directory, `${name}.png`) } : undefined); return crypto.createHash('sha256').update(image).digest('hex'); } finally { await page.evaluate(() => window.__restoreCanvasCapture?.()); } }; try { await page.goto('http://127.0.0.1:4189', { waitUntil: 'domcontentloaded' }); await page.getByLabel('Username').fill('Phenom'); await page.getByLabel('Password').fill('dungeon-route-admin'); await page.getByRole('button', { name: /Create account/ }).click(); await page.getByRole('button', { name: /Create character/ }).click(); await page.getByRole('button', { name: /Customize & name/ }).click(); await page.getByLabel('Character name').fill('Routewatcher'); await page.getByRole('button', { name: /Create character/ }).click(); await page.getByRole('button', { name: /Continue/ }).first().click(); await page.getByRole('button', { name: /GM Tools/ }).click(); await page.getByRole('heading', { name: 'Dungeon route simulations' }).waitFor(); await capture('01-gm-route-launch'); const routeSelects = page.locator('.dungeon-sim-admin__catalog select'); await routeSelects.nth(0).selectOption('wailing-caverns'); await routeSelects.nth(1).selectOption('navigation-only'); await page.locator('.dungeon-sim-admin__catalog input').fill('browser-smoke-fixed-seed'); await page.getByRole('button', { name: 'Launch spectate mode' }).click(); await page.locator('canvas').waitFor(); await page.getByLabel('Dungeon simulation controls').waitFor(); await page.waitForTimeout(6000); await capture('02-live-navigation-simulation'); await page.getByRole('button', { name: 'Pause' }).click(); const pausedTime = await page.locator('.dungeon-sim-hud__header > time').innerText(); await page.waitForTimeout(700); assert.equal(await page.locator('.dungeon-sim-hud__header > time').innerText(), pausedTime, 'pause freezes simulation time'); await page.getByRole('button', { name: 'Step' }).click(); await page.getByRole('button', { name: 'Step' }).click(); await page.waitForTimeout(200); assert.notEqual(await page.locator('.dungeon-sim-hud__header > time').innerText(), pausedTime, 'single-step advances simulation time'); const memberPicker = page.getByLabel('Spectate party member'); const memberButtons = memberPicker.locator('div > button'); assert.equal(await memberButtons.count(), 5, 'all five simulated party members are spectatable'); const chosenName = await memberButtons.nth(2).locator('strong').innerText(); await memberButtons.nth(2).click(); assert.equal(await memberButtons.nth(2).getAttribute('aria-pressed'), 'true'); await page.waitForTimeout(300); await capture('03-follow-selected-member'); const beforeFollowArrowLook = await page.evaluate(() => window.readSpectatorCameraState()); await page.keyboard.down('ArrowLeft'); await page.waitForTimeout(300); await page.keyboard.up('ArrowLeft'); const afterFollowArrowLook = await page.evaluate(() => window.readSpectatorCameraState()); assert.notEqual(afterFollowArrowLook.yaw, beforeFollowArrowLook.yaw, 'ArrowLeft orbits the follow camera'); await page.getByRole('button', { name: 'Free camera' }).click(); assert.equal(await page.getByRole('button', { name: 'Free camera' }).getAttribute('aria-pressed'), 'true'); const beforeFlight = await captureCanvas('03-canvas-before-flight'); await page.keyboard.down('w'); await page.waitForTimeout(700); await page.keyboard.up('w'); await page.waitForTimeout(150); const afterFlight = await captureCanvas(); assert.notEqual(afterFlight, beforeFlight, 'free-camera movement redraws the 3D dungeon without relying on nameplate overlays'); const beforeArrowLook = await page.evaluate(() => window.readSpectatorCameraState()); await page.keyboard.down('ArrowRight'); await page.waitForTimeout(300); await page.keyboard.up('ArrowRight'); const afterArrowLook = await page.evaluate(() => window.readSpectatorCameraState()); assert.notEqual(afterArrowLook.yaw, beforeArrowLook.yaw, 'ArrowRight turns the free camera'); const canvasBounds = await page.locator('canvas').boundingBox(); assert(canvasBounds, 'simulation canvas is visible'); const lookStart = { x: canvasBounds.x + canvasBounds.width * 0.5, y: canvasBounds.y + canvasBounds.height * 0.55 }; const beforeLook = await page.evaluate(() => window.readSpectatorCameraState()); await page.mouse.move(lookStart.x, lookStart.y); await page.mouse.down({ button: 'right' }); await page.mouse.move(lookStart.x + 220, lookStart.y + 100, { steps: 12 }); await page.waitForTimeout(250); const duringLook = await page.evaluate(() => window.readSpectatorCameraState()); assert.notEqual(duringLook.yaw, beforeLook.yaw, 'right-drag rotates the free camera'); assert.equal(duringLook.pointerLocked, false, 'right-drag does not request Pointer Lock'); await page.mouse.up({ button: 'right' }); const releasedLook = await page.evaluate(() => window.readSpectatorCameraState()); await page.mouse.move(lookStart.x - 120, lookStart.y + 50, { steps: 8 }); await page.waitForTimeout(250); assert.equal( (await page.evaluate(() => window.readSpectatorCameraState())).yaw, releasedLook.yaw, 'mouse movement after right-button release does not rotate the camera', ); await page.locator('canvas').screenshot({ path: path.join(directory, '04-canvas-after-flight-look.png') }); await capture('04-paused-free-camera-moved'); await memberButtons.nth(4).click(); assert.equal(await page.getByRole('button', { name: 'Follow member' }).getAttribute('aria-pressed'), 'true'); assert.match(await memberPicker.locator('header strong').innerText(), /\S/); assert.notEqual(await memberPicker.locator('header strong').innerText(), chosenName); await page.waitForTimeout(300); await capture('05-follow-another-member'); await page.getByRole('button', { name: '4×' }).click(); await page.getByRole('button', { name: 'Play' }).click(); await page.waitForTimeout(1200); await page.getByRole('button', { name: 'Stop & save' }).click(); await page.getByRole('button', { name: 'Return to GM Tools' }).waitFor(); await capture('06-finished-report'); await page.getByRole('button', { name: 'Return to GM Tools' }).click(); await page.getByRole('heading', { name: 'Dungeon route simulations' }).waitFor(); await page.locator('.dungeon-sim-admin__catalog select').nth(0).selectOption('wailing-caverns'); await page.getByText(/1 runs for this route/).waitFor(); await capture('07-saved-replay'); await page.getByRole('button', { name: 'Watch' }).first().click(); await page.getByText('Recorded replay').waitFor(); await page.getByLabel('Replay position').waitFor(); await page.waitForTimeout(6000); await page.getByRole('button', { name: 'Play' }).click(); await page.waitForTimeout(800); await capture('08-replay-controls'); await page.getByRole('button', { name: 'Pause' }).click(); const pausedReplayTime = await page.locator('.dungeon-sim-hud__header > time').innerText(); await page.waitForTimeout(500); assert.equal(await page.locator('.dungeon-sim-hud__header > time').innerText(), pausedReplayTime, 'replay pause freezes playback time'); await page.getByRole('button', { name: '4×' }).click(); await page.getByRole('button', { name: 'Play' }).click(); await page.waitForTimeout(600); await capture('09-replay-4x'); assert.deepEqual(errors, [], `No browser page errors: ${errors.join('; ')}`); console.log('Dungeon simulation browser smoke test passed.'); } finally { await browser.close(); await new Promise((resolve) => runtime.server.close(resolve)); } })().catch((error) => { console.error(error); process.exitCode = 1; });