123/** Verify that the public player owns the whole viewport without editor DOM chrome. */4import assert from 'node:assert/strict';56const configuredPlaywright = process.env.PLAYWRIGHT_MODULE || 'playwright';7const playwrightModule = configuredPlaywright.startsWith('/')8 ? `${configuredPlaywright.replace(/\/$/, '')}${configuredPlaywright.endsWith('.js') ? '' : '/index.js'}`9 : configuredPlaywright;10const imported = await import(playwrightModule);11const { chromium } = imported.default || imported;12const baseUrl = process.env.MIAU_URL || 'http://127.0.0.1:5173/';13const browser = await chromium.launch({ headless: true });14const errors = [];1516function scene(page) {17 return page.locator('.stage-shell').getAttribute('data-scene');18}1920async function waitScene(page, value) {21 await page.waitForFunction(expected => document.querySelector('.stage-shell')?.dataset.scene === expected, value);22}2324async function assertPlayerChrome(page, label) {25 const metrics = await page.evaluate(() => {26 const html = getComputedStyle(document.documentElement);27 const body = getComputedStyle(document.body);28 const canvas = document.querySelector('.stage-canvas')?.getBoundingClientRect();29 return {30 scrollHeight: document.scrollingElement?.scrollHeight,31 innerHeight,32 htmlHeight: html.height,33 bodyHeight: body.height,34 htmlOverflow: html.overflow,35 bodyOverflow: body.overflow,36 htmlOverscroll: html.overscrollBehavior,37 bodyOverscroll: body.overscrollBehavior,38 canvas,39 appDisplay: getComputedStyle(document.querySelector('.app-shell')).display,40 };41 });42 assert.equal(metrics.scrollHeight, metrics.innerHeight, `${label}: player must not scroll`);43 assert.equal(metrics.htmlOverflow, 'hidden', `${label}: html overflow`);44 assert.equal(metrics.bodyOverflow, 'hidden', `${label}: body overflow`);45 assert.equal(metrics.htmlOverscroll, 'none', `${label}: html overscroll`);46 assert.equal(metrics.bodyOverscroll, 'none', `${label}: body overscroll`);47 assert.equal(metrics.appDisplay, 'none', `${label}: editor shell must be hidden`);48 assert.deepEqual(49 { x: metrics.canvas.x, y: metrics.canvas.y, width: metrics.canvas.width, height: metrics.canvas.height },50 { x: 0, y: 0, width: await page.evaluate(() => innerWidth), height: metrics.innerHeight },51 `${label}: canvas must cover the viewport`,52 );53}5455async function runViewport(viewport) {56 const context = await browser.newContext({ viewport, isMobile: viewport.width < 500, hasTouch: viewport.width < 500 });57 const page = await context.newPage();58 page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); });59 page.on('pageerror', error => errors.push(error.message));60 await page.goto(baseUrl, { waitUntil: 'networkidle' });61 assert.equal(await page.locator('body').evaluate(node => node.classList.contains('stage-player')), true);62 await waitScene(page, 'title'); await assertPlayerChrome(page, `${viewport.width} title`);63 await page.keyboard.press('Enter'); await waitScene(page, 'index'); await assertPlayerChrome(page, `${viewport.width} index`);64 await page.keyboard.press('Enter'); await waitScene(page, 'proof'); await assertPlayerChrome(page, `${viewport.width} proof`);65 await page.keyboard.press('Escape'); await waitScene(page, 'pause'); await assertPlayerChrome(page, `${viewport.width} pause`);66 await page.keyboard.press('Escape'); await waitScene(page, 'proof');67 const winningRoute = [68 'ArrowRight', 'ArrowRight', 'ArrowUp', 'ArrowUp',69 ...Array(4).fill('ArrowDown'), ...Array(7).fill('ArrowRight'),70 ];71 for (const key of winningRoute) await page.keyboard.press(key);72 await page.waitForFunction(() => JSON.parse(window.render_game_to_text()).mode === 'won');73 await waitScene(page, 'win');74 await assertPlayerChrome(page, `${viewport.width} win`);75 await context.close();76}7778try {79 await runViewport({ width: 1280, height: 800 });80 await runViewport({ width: 390, height: 844 });81 assert.deepEqual(errors, [], 'public player should have no browser errors');82 console.log('✓ public player viewport chrome is hidden and non-scrolling at desktop/mobile');83} finally {84 await browser.close();85}