From b1ce1927022d374198441afafeb27199fc8a052a Mon Sep 17 00:00:00 2001 From: eunhhu Date: Thu, 24 Sep 2026 17:14:54 +0100 Subject: Establish native micro runtime checkpoint --- prototypes/live-rules/qa-browser.js | 131 ++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 prototypes/live-rules/qa-browser.js (limited to 'prototypes/live-rules/qa-browser.js') diff --git a/prototypes/live-rules/qa-browser.js b/prototypes/live-rules/qa-browser.js new file mode 100644 index 0000000..ee11c3a --- /dev/null +++ b/prototypes/live-rules/qa-browser.js @@ -0,0 +1,131 @@ +// Pass this expression to Playwright CLI's run-code after opening the local demo. +// Interactions use real UI controls; the read-only snapshot only observes results. +async (page) => { + const checks = []; + const errors = []; + const onError = (error) => errors.push(error.message); + const check = (condition, label) => { + if (!condition) throw new Error(label); + checks.push(label); + }; + const state = () => page.evaluate(() => window.pureDemo.snapshot()); + const button = (name) => page.getByRole('button', { name, exact: true }); + const step = async (count) => { for (let i = 0; i < count; i++) await button('한 프레임').click(); }; + page.on('pageerror', onError); + try { + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.reload(); + await page.waitForFunction(() => window.pureDemo?.snapshot().ready); + const initial = await state(); + check(initial.running === 0 && initial.tick === 0 && initial.revision === 0, 'initial state is paused'); + + await page.locator('#stage').focus(); + await page.keyboard.press('Space'); + let s = await state(); + check(s.accepted === 1 && s.inputs === 1 && s.source === 0 && s.y === 0, 'Space goes through keyboard input once while paused'); + await step(8); + await button('점프 Space').click(); + const before = await state(); + check(before.tick === 8 && before.y === 6020 && before.vy === 560 && before.reason === 3, 'ground rule rejects a second airborne jump'); + await page.getByRole('radio', { name: '두 번 점프 공중에서 한 번 더 뛸 수 있어요.', exact: true }).check(); + const draft = await state(); + check(draft.mode === 0 && draft.revision === 0 && draft.draftDirty, 'draft changes do not execute before Apply'); + await button('규칙 적용').click(); + const applied = await state(); + check(['tick', 'y', 'vy', 'distance', 'obstacleX', 'jumps'].every((key) => applied[key] === before[key]), 'applying rules preserves exact simulation progress'); + check(applied.mode === 1 && applied.revision === 1 && applied.decisionRevision === 0 && await page.locator('#stale-decision').isVisible(), 'old decision retains and displays its revision'); + await button('점프 Space').click(); + const second = await state(); + check(second.reason === 1 && second.jumps === 2 && second.source === 1 && second.decisionRevision === 1, 'pointer input uses new rule and accepts second jump'); + await button('점프 Space').click(); + check((await state()).reason === 4, 'third jump is rejected'); + + await page.getByRole('checkbox').uncheck(); + await button('규칙 적용').click(); + await button('점프 Space').click(); + check((await state()).reason === 2, 'disabled rule explains rejection'); + await button('처음부터').click(); + s = await state(); + check(s.tick === 0 && s.mode === 0 && s.impulse === 10 && s.enabled === 1 && s.inputs === 0 && s.running === 0, 'explicit reset restores rules and state'); + + await page.getByRole('slider', { name: '점프 세기' }).focus(); + await page.keyboard.press('Space'); + await page.getByRole('checkbox').focus(); + await page.keyboard.press('Space'); + check((await state()).inputs === 0, 'Space in form controls never dispatches a jump'); + await button('처음부터').click(); + await button('점프 Space').focus(); + await page.keyboard.press('Space'); + check((await state()).inputs === 1, 'Space on the jump button does not double dispatch'); + await button('처음부터').click(); + await page.locator('#stage').focus(); + await page.keyboard.down('Space'); + await page.keyboard.down('Space'); + await page.keyboard.up('Space'); + check((await state()).inputs === 1, 'keyboard repeat does not add a second request'); + + await button('처음부터').click(); + await button('실행').click(); + await page.waitForFunction(() => window.pureDemo.snapshot().tick >= 6); + await button('일시정지').click(); + const paused = await state(); + await page.waitForTimeout(120); + check(paused.tick >= 6 && paused.tick === (await state()).tick, 'running advances and pause freezes simulation'); + + await button('처음부터').click(); + await page.getByRole('radio', { name: '두 번 점프 공중에서 한 번 더 뛸 수 있어요.', exact: true }).check(); + await page.getByRole('slider', { name: '점프 세기' }).focus(); + await page.keyboard.press('End'); + await button('규칙 적용').click(); + await button('점프 Space').click(); + await step(28); + await button('점프 Space').click(); + await step(29); + const high = await state(); + const visible = await page.evaluate(() => { + const d = document.getElementById('dino').getBoundingClientRect(); + const w = document.getElementById('world').getBoundingClientRect(); + return d.top >= w.top && d.bottom <= w.bottom && d.left >= w.left && d.right <= w.right; + }); + check(high.y > 40000 && visible, 'maximum-height double jump remains inside the scene'); + + for (let i = 0; i < 25; i++) await button('점프 Space').click(); + check(await page.locator('#history-list li').count() === 8, 'visible history is bounded to eight rows'); + check((await state()).wasmMemoryBytes === 65536, 'browser instance retains fixed 64 KiB linear memory'); + await page.locator('.research-panel summary').click(); + check((await page.locator('#bytes-total').textContent()).includes('396'), 'research panel shows logical layout separately'); + + await page.setViewportSize({ width: 390, height: 844 }); + await page.locator('.research-panel summary').click(); + await button('처음부터').click(); + const width = await page.evaluate(() => ({ viewport: innerWidth, content: document.documentElement.scrollWidth })); + check(width.content <= width.viewport, '390px viewport has no horizontal overflow'); + check(await page.locator('#dino').evaluate((el) => el.getBoundingClientRect().height) >= 28, 'narrow view keeps the dinosaur large enough to inspect'); + await button('점프 Space').click(); + await step(1); + check((await state()).y === 945, 'narrow viewport controls remain usable'); + await page.screenshot({ path: 'output/playwright/pure-live-mobile.png', fullPage: true }); + + await page.route('**/core.wasm', (route) => route.abort()); + await page.reload(); + await page.locator('#load-error').waitFor({ state: 'visible' }); + check(await button('실행').isDisabled(), 'missing Wasm has an actionable error and disabled controls'); + await page.unroute('**/core.wasm'); + await page.reload(); + await page.waitForFunction(() => window.pureDemo?.snapshot().ready); + await page.setViewportSize({ width: 1440, height: 1000 }); + await button('점프 Space').click(); + await step(8); + await button('점프 Space').click(); + await page.getByRole('radio', { name: '두 번 점프 공중에서 한 번 더 뛸 수 있어요.', exact: true }).check(); + await button('규칙 적용').click(); + await button('점프 Space').click(); + await page.screenshot({ path: 'output/playwright/pure-live-desktop.png', fullPage: true }); + check(errors.length === 0, 'no uncaught page errors'); + return { recordedUtc: new Date().toISOString(), browser: await page.evaluate(() => navigator.userAgent), + checks, before, applied, second, high, mobile: width, errors, + screenshots: ['output/playwright/pure-live-desktop.png', 'output/playwright/pure-live-mobile.png'] }; + } finally { + page.off('pageerror', onError); + } +} -- cgit v1.2.3