- commit
- 82694916a231c0050c73bff6712cc696c2d885bb
- parent
- be7f4a60205ed7bc801386307ba97a4547a13784
- author
- Dami <dami.etoile@gmail.com>
| M | tests/ | 151 | ++++++++++++++++++++++++++++++- |
@@ -77,6 +77,20 @@ module.exports = {
7777 `;7878 7979 const invalidFixture = `module.exports = { manifest: { name: 'Broken pack' }, levels: [] };\n`;80+const selfWinFixture = `81+module.exports = {82+ manifest: { id: 'browser-self-win', name: 'Browser Self Win', version: '1.0.0' },83+ vocabulary: ['MIAU'],84+ palette: { MIAU: '🐈' },85+ levels: [{ id: 'self-win', title: 'Self Win', hint: 'MIAU IS YOU and MIAU IS WIN.', width: 5, height: 3,86+ objects: [87+ { kind: 'word', text: 'MIAU', x: 0, y: 0 }, { kind: 'word', text: 'IS', x: 1, y: 0 }, { kind: 'word', text: 'YOU', x: 2, y: 0 },88+ { kind: 'word', text: 'MIAU', x: 0, y: 2 }, { kind: 'word', text: 'IS', x: 1, y: 2 }, { kind: 'word', text: 'WIN', x: 2, y: 2 },89+ { kind: 'object', noun: 'MIAU', x: 0, y: 1 },90+ ],91+ }],92+};93+`;8094 8195 function report(label) {8296 process.stdout.write(`✓ ${label}\n`);@@ -239,9 +253,11 @@ await mkdir(stageScreenshotDir, { recursive: true });
239253 const firstPath = resolve(tempDir, 'browser-smoke-one.cjs');240254 const secondPath = resolve(tempDir, 'browser-smoke-two.cjs');241255 const invalidPath = resolve(tempDir, 'browser-smoke-invalid.cjs');256+const selfWinPath = resolve(tempDir, 'browser-self-win.cjs');242257 await writeFile(firstPath, fixtureOne);243258 await writeFile(secondPath, fixtureTwo);244259 await writeFile(invalidPath, invalidFixture);260+await writeFile(selfWinPath, selfWinFixture);245261 246262 const browser = await chromium.launch({ headless: !headed });247263 const context = await browser.newContext({ acceptDownloads: true });@@ -262,9 +278,34 @@ try {
262278 assert.equal(await stagePage.locator('body').evaluate(node => node.classList.contains('stage-player')), true, 'default route should enter stage player mode');263279 assert.equal(await stagePage.locator('.stage-canvas').count(), 1, 'default route should render one stage canvas');264280 assert.equal(await stagePage.locator('.stage-shell').getAttribute('data-scene'), 'title', 'default route should open on the title scene');281+ await stagePage.keyboard.press('Space');282+ await stagePage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'workshop');283+ await stagePage.keyboard.press('Escape');284+ await stagePage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'title');285+ report('title Space opens Workshop and Escape returns to title');265286 await stagePage.keyboard.press('Enter');266287 await stagePage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'index');267288 await stagePage.screenshot({ path: `${stageScreenshotDir}/index.png` });289+ await stagePage.keyboard.press('Space');290+ await stagePage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'workshop');291+ await stagePage.keyboard.press('Escape');292+ await stagePage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'index');293+ report('INDEX Space opens Workshop and Escape returns to the same INDEX');294+ const pointerContext = await browser.newContext();295+ const pointerPage = await pointerContext.newPage();296+ pointerPage.setDefaultTimeout(timeout);297+ await pointerPage.setViewportSize({ width: 1280, height: 800 });298+ await pointerPage.goto(playerUrl, { waitUntil: 'networkidle' });299+ await pointerPage.keyboard.press('Enter');300+ await pointerPage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'index');301+ await pointerPage.waitForTimeout(900);302+ await pointerPage.mouse.click(1035, 760);303+ await pointerPage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'workshop');304+ await pointerPage.keyboard.press('Escape');305+ await pointerPage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'index');306+ await pointerPage.close();307+ await pointerContext.close();308+ report('INDEX Workshop footer pointer opens Workshop and Escape returns');268309 await stagePage.keyboard.press('Enter');269310 await stagePage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'proof');270311 await stagePage.screenshot({ path: `${stageScreenshotDir}/proof.png` });@@ -283,6 +324,14 @@ try {
283324 assert.ok(progressKeys.some(key => key.includes('core/')), 'winning should persist core proof progress');284325 await stagePage.keyboard.press('Enter');285326 await stagePage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'index');327+ await stagePage.keyboard.press('ArrowRight');328+ await stagePage.keyboard.press('Enter');329+ await stagePage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'proof');330+ const playerProofTwo = JSON.parse(await stagePage.evaluate(() => window.render_game_to_text()));331+ const playerMiauTwo = playerProofTwo.objects.find(object => object.kind === 'object' && object.noun === 'MIAU');332+ assert.equal(playerProofTwo.level, 'Make room to push', 'player Win Enter→INDEX→next should open proof 02');333+ assert.deepEqual({ x: playerMiauTwo?.x, y: playerMiauTwo?.y }, { x: 1, y: 5 }, 'player proof 02 first frame should place MIAU at its authored coordinate');334+ await stagePage.screenshot({ path: `${stageScreenshotDir}/player-proof-02-first-frame.png` });286335 const progressBeforeReload = await stagePage.evaluate(() => Object.keys(localStorage).filter(key => key.startsWith('miau.index.progress/')));287336 await stagePage.reload({ waitUntil: 'networkidle' });288337 const progressAfterReload = await stagePage.evaluate(() => Object.keys(localStorage).filter(key => key.startsWith('miau.index.progress/')));@@ -406,6 +455,33 @@ try {
406455 report('Workshop remove cancel, confirm, and reload persistence work');407456 await workshopPage.close();408457 458+ const returnFlowContext = await browser.newContext();459+ const returnFlowPage = await returnFlowContext.newPage();460+ returnFlowPage.setDefaultTimeout(timeout);461+ await returnFlowPage.goto(playerUrl, { waitUntil: 'networkidle' });462+ await returnFlowPage.keyboard.press('Enter');463+ await returnFlowPage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'index');464+ await returnFlowPage.keyboard.press('Space');465+ await returnFlowPage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'workshop');466+ await importLocalModule(returnFlowPage, secondPath, { install: false });467+ await returnFlowPage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'gift');468+ await returnFlowPage.keyboard.press('Escape');469+ await returnFlowPage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'workshop');470+ report('local import Gift Not now returns to Workshop');471+ await returnFlowPage.close();472+ await returnFlowContext.close();473+474+ const sharedReturnContext = await browser.newContext();475+ const sharedReturnPage = await sharedReturnContext.newPage();476+ sharedReturnPage.setDefaultTimeout(timeout);477+ await sharedReturnPage.goto(shareUrl, { waitUntil: 'networkidle' });478+ await sharedReturnPage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'gift');479+ await sharedReturnPage.keyboard.press('Escape');480+ await sharedReturnPage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'title');481+ report('shared URL Gift Not now returns to title');482+ await sharedReturnPage.close();483+ await sharedReturnContext.close();484+409485 const sharedPage = await context.newPage();410486 sharedPage.setDefaultTimeout(timeout);411487 const sharedDevUrl = new URL(shareUrl); sharedDevUrl.searchParams.set('dev', '1');@@ -539,6 +615,79 @@ try {
539615 await singlePackPage.close();540616 await singlePackContext.close();541617 618+ const continueContext = await browser.newContext();619+ const continuePage = await continueContext.newPage();620+ continuePage.setDefaultTimeout(timeout);621+ await continuePage.goto(devUrl, { waitUntil: 'networkidle' });622+ await openDevProof(continuePage);623+ for (const key of ['ArrowRight', 'ArrowRight', 'ArrowUp', 'ArrowUp', ...Array(4).fill('ArrowDown'), ...Array(7).fill('ArrowRight')]) await continuePage.keyboard.press(key);624+ await continuePage.waitForFunction(() => JSON.parse(window.render_game_to_text()).mode === 'won');625+ await continuePage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'win');626+ const firstProof = JSON.parse(await continuePage.evaluate(() => window.render_game_to_text()));627+ await continuePage.locator('#continue-btn').click();628+ await continuePage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'proof');629+ const secondProof = JSON.parse(await continuePage.evaluate(() => window.render_game_to_text()));630+ assert.notEqual(secondProof.level, firstProof.level, 'Continue should load the next proof');631+ assert.equal(secondProof.mode, 'play', 'Continue should reset the next proof into play mode');632+ const secondProofMiau = secondProof.objects.find(object => object.kind === 'object' && object.noun === 'MIAU');633+ assert.deepEqual({ x: secondProofMiau?.x, y: secondProofMiau?.y }, { x: 1, y: 5 }, 'Continue proof 02 first frame should place MIAU at its authored coordinate');634+ await continuePage.screenshot({ path: `${stageScreenshotDir}/continue-proof-02.png` });635+ await continuePage.keyboard.press('ArrowRight');636+ await continuePage.waitForFunction(() => JSON.parse(window.render_game_to_text()).moves > 0);637+ report('Continue enters proof 02 and accepts the first movement input');638+ await continuePage.close();639+ await continueContext.close();640+641+ const proofThreeContext = await browser.newContext();642+ const proofThreePage = await proofThreeContext.newPage();643+ proofThreePage.setDefaultTimeout(timeout);644+ await proofThreePage.goto(devUrl, { waitUntil: 'networkidle' });645+ await openDevProof(proofThreePage);646+ const proofThreeChoice = proofThreePage.locator('#level-list [data-level-id*="two-things-can-win"]');647+ assert.equal(await proofThreeChoice.count(), 1, 'proof 03 selector should identify exactly one level');648+ assert.equal(await proofThreeChoice.getAttribute('data-level-id'), 'core:two-things-can-win', 'proof 03 should be the core two-things level');649+ await proofThreeChoice.click();650+ const proofThreeInitial = JSON.parse(await proofThreePage.evaluate(() => window.render_game_to_text()));651+ assert.equal(proofThreeInitial.mode, 'play', 'proof 03 should start unwon');652+ assert.ok(proofThreeInitial.rules.some(rule => /ROCK.*WIN|FLAG.*WIN/.test(rule.text)), 'proof 03 should expose the multi-object WIN rule');653+ await proofThreePage.keyboard.press('ArrowRight');654+ await proofThreePage.waitForTimeout(100);655+ const proofThreeAfterMove = JSON.parse(await proofThreePage.evaluate(() => window.render_game_to_text()));656+ assert.equal(proofThreeAfterMove.mode, 'play', 'proof 03 should remain unwon after the first movement');657+ await proofThreePage.screenshot({ path: `${stageScreenshotDir}/proof-03-initial.png` });658+ report('proof 03 starts unwon with the multi-object WIN rule');659+ await proofThreeChoice.click();660+ await proofThreePage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'proof');661+ const selfWinMoves = 'RRRRRDRRRRRDLLLLLLLLLLLUUURRRRUULLLLDRRDRUU';662+ for (const key of selfWinMoves) await proofThreePage.keyboard.press(`Arrow${({ R: 'Right', L: 'Left', U: 'Up', D: 'Down' })[key]}`);663+ await proofThreePage.waitForFunction(() => JSON.parse(window.render_game_to_text()).mode === 'won');664+ await proofThreePage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'win');665+ const proofThreeWin = JSON.parse(await proofThreePage.evaluate(() => window.render_game_to_text()));666+ assert.ok(proofThreeWin.rules.some(rule => /MIAU.*WIN/.test(rule.text)), 'proof 03 self-win should finish with MIAU IS WIN');667+ await proofThreePage.screenshot({ path: `${stageScreenshotDir}/proof-03-self-win.png` });668+ report('proof 03 real sequence reaches self-win and opens the Win card');669+ await proofThreePage.close();670+ await proofThreeContext.close();671+672+ const selfWinContext = await browser.newContext();673+ const selfWinPage = await selfWinContext.newPage();674+ selfWinPage.setDefaultTimeout(timeout);675+ await selfWinPage.goto(devUrl, { waitUntil: 'networkidle' });676+ await openDevProof(selfWinPage);677+ await importLocalModule(selfWinPage, selfWinPath);678+ await selfWinPage.waitForFunction(() => document.body.textContent.includes('Browser Self Win'));679+ await selectPack(selfWinPage, 'browser-self-win');680+ await selfWinPage.keyboard.press('Enter');681+ await selfWinPage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'proof');682+ const selfWinInitial = JSON.parse(await selfWinPage.evaluate(() => window.render_game_to_text()));683+ assert.equal(selfWinInitial.mode, 'won', 'self-win fixture should settle as won on its first frame');684+ await selfWinPage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'win');685+ assert.equal(JSON.parse(await selfWinPage.evaluate(() => window.render_game_to_text())).mode, 'won', 'MIAU IS YOU plus MIAU IS WIN should reach the Win card');686+ await selfWinPage.screenshot({ path: `${stageScreenshotDir}/self-win-card.png` });687+ report('self-win fixture reaches the Win card after MIAU IS YOU and MIAU IS WIN settle');688+ await selfWinPage.close();689+ await selfWinContext.close();690+542691 const mobilePage = await context.newPage();543692 mobilePage.setDefaultTimeout(timeout);544693 mobilePage.on('console', message => { if (message.type() === 'error') consoleErrors.push(`mobile: ${message.text()}`); });@@ -581,7 +730,7 @@ try {
581730 const indexCanvas = accessibilityPage.locator('.stage-canvas');582731 const indexBox = await indexCanvas.boundingBox();583732 assert.ok(indexBox, 'mobile INDEX should expose a tappable canvas');584- await accessibilityPage.mouse.click(indexBox.x + 64, indexBox.y + 374);733+ await accessibilityPage.mouse.click(indexBox.x + 64, indexBox.y + 405);585734 await accessibilityPage.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'proof');586735 report('mobile reduced-motion title key and INDEX touch entry work without audio');587736 assert.deepEqual(accessibilityErrors, [], 'missing audio should not produce browser errors');