1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
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);
}
}
|