194 lines
11 KiB
JavaScript
194 lines
11 KiB
JavaScript
const { DEFAULT_TEST_DUNGEON_ID } = require('./playtest-defaults.cjs');
|
||
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(DEFAULT_TEST_DUNGEON_ID);
|
||
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(DEFAULT_TEST_DUNGEON_ID);
|
||
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;
|
||
});
|