- commit
- 136662e7c14af7d6fe427071d4366c1037ae4422
- parent
- a1e71dd425a2a7e13a1bfd13d720cfe14bf394f3
- author
- Dami <dami.etoile@gmail.com>
| M | openspec/ | 4 | ++ |
| M | src/ | 8 | +++- |
| A | tests/ | 63 | ++++++++++++++++++++++++++++++ |
@@ -11,6 +11,10 @@ The stage SHALL support Title, INDEX, Proof, Pause, Win card, Workshop, and pend
1111 - **WHEN** a second key or tap arrives while Pause is exiting1212 - **THEN** only the first captured action runs and navigation does not resume until the destination scene is ready1313 14+#### Scenario: An external import finishes during menu exit15+- **WHEN** a file import finishes while a menu is exiting and requests Gift or Workshop16+- **THEN** the current exit completes and the latest externally requested scene opens, while player menu input remains ignored during the exit17+1418 #### Scenario: Toggle remains in Pause1519 - **WHEN** the player selects Sound or Music1620 - **THEN** that item presses and updates in place while Pause remains open@@ -497,7 +497,13 @@ export function createStage(options = {}) {
497497 }498498 }499499 function show(id, data = {}) {500- if (sceneMotion.phase === 'exiting') return api;500+ if (sceneMotion.phase === 'exiting') {501+ // External events such as a completed file import can arrive while a502+ // menu is leaving. Keep the current exit animation and enter the latest503+ // requested scene when it finishes. Menu input is blocked separately.504+ sceneMotion = { ...sceneMotion, target: id, data };505+ return api;506+ }501507 const menuScene = scene === 'title' || scene === 'index' || scene === 'gift' || scene === 'workshop';502508 if (!data.instant && !reducedMotion && menuScene && id !== scene) {503509 const now = visualNow();@@ -0,0 +1,63 @@
1+2+3+/**4+ * Regression check for a valid local import arriving during an invalid import's5+ * INDEX -> Workshop error transition.6+ *7+ * Usage:8+ * PLAYWRIGHT_MODULE=/path/to/playwright node tests/browser/import-transition-race.mjs9+ * MIAU_URL=http://127.0.0.1:5192/ ... node tests/browser/import-transition-race.mjs10+ */11+12+import assert from 'node:assert/strict';13+import { mkdtemp, rm, writeFile } from 'node:fs/promises';14+import { tmpdir } from 'node:os';15+import { resolve } from 'node:path';16+17+const configuredPlaywright = process.env.PLAYWRIGHT_MODULE || 'playwright';18+const playwrightModule = configuredPlaywright.startsWith('/')19+ ? `${configuredPlaywright.replace(/\/$/, '')}${configuredPlaywright.endsWith('.js') ? '' : '/index.js'}`20+ : configuredPlaywright;21+const playwright = await import(playwrightModule);22+const { chromium } = playwright.default || playwright;23+24+const projectRoot = resolve(new URL('..', import.meta.url).pathname, '..');25+const baseUrl = process.env.MIAU_URL || 'http://127.0.0.1:5173/';26+const timeout = Number(process.env.BROWSER_TIMEOUT || 8000);27+const tempDir = await mkdtemp(`${tmpdir()}/miau-import-transition-`);28+const invalidPath = resolve(tempDir, 'invalid.cjs');29+const validPath = resolve(projectRoot, 'mods/example.cjs');30+await writeFile(invalidPath, 'module.exports = null;\n');31+32+const browser = await chromium.launch({ headless: true });33+const context = await browser.newContext({ viewport: { width: 1280, height: 800 } });34+const page = await context.newPage();35+const errors = [];36+page.on('console', message => {37+ if (message.type() === 'error') errors.push(`console: ${message.text()}`);38+});39+page.on('pageerror', error => errors.push(`page: ${error.message}`));40+41+try {42+ await page.goto(`${baseUrl.replace(/\/$/, '')}/?dev=1`, { waitUntil: 'networkidle' });43+ const scene = () => page.locator('.stage-shell').getAttribute('data-scene');44+ await page.keyboard.press('Enter');45+ await page.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'index', null, { timeout });46+ await page.waitForTimeout(500);47+48+ const input = page.locator('#mod-input');49+ await input.setInputFiles(invalidPath);50+ // Deliberately dispatch the valid import before the invalid error transition51+ // has finished. The valid pack must become the pending destination.52+ await input.setInputFiles(validPath);53+ await page.waitForFunction(() => document.querySelector('.stage-shell')?.dataset.scene === 'gift', null, { timeout });54+55+ assert.equal(await scene(), 'gift', 'valid import should win over the pending Workshop error transition');56+ assert.match(await page.locator('#gift-title, body').first().textContent(), /Moon Cat|Gift/i);57+ assert.deepEqual(errors, [], 'import transition should not emit browser errors');58+ console.log('✓ invalid → valid local import resolves to Gift with no browser errors');59+} finally {60+ await context.close();61+ await browser.close();62+ await rm(tempDir, { recursive: true, force: true });63+}