aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes/workbench/results/2026-09-23-pointer/qa-browser.js
blob: 9c2514aff43b1b3997ce972b5729044427877200 (plain)
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
// Run this expression with Playwright CLI run-code after opening the local host.
// All behavior is authored through the editor; snapshot() only observes results.
async (page) => {
  const checks = [];
  const errors = [];
  const onError = error => errors.push(error.message);
  const acceptDialog = dialog => dialog.accept();
  const check = (condition, label) => {
    if (!condition) throw new Error(`Check ${checks.length + 1} failed: ${label}`);
    checks.push(label);
  };
  const state = () => page.evaluate(() => window.pureWorkbench.snapshot());
  const apply = async text => { await page.locator('#source').fill(text); await page.locator('#apply').click(); };
  const authorSource = [
    '# A new authored interaction, not a bundled example.',
    'state x = 90', 'state moving = 0', 'state count = 0',
    'input advance = ArrowRight', 'input pulse = Space',
    'box marker(x, 170, 48, 48, "#7ed9ae")',
    'text reading(40, 70, count, "Signals: ")',
    'on advance.down { moving = 1 }', 'on advance.up { moving = 0 }',
    'on pulse.down { count = count + 1 }',
    'on frame { x = clamp(x + moving * 120 * dt, 0, 580) }', '',
  ].join('\n');
  let lastAuthored = null;
  let mobile = null;
  let downloaded = null;
  const shots = ['output/playwright/pure-pointer-workbench-blank.png', 'output/playwright/pure-pointer-workbench-authored.png', 'output/playwright/pure-pointer-workbench-mobile.png'];
  page.on('pageerror', onError);
  page.on('dialog', acceptDialog);
  try {
    await page.setViewportSize({ width: 1440, height: 1000 });
    await page.reload();
    await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready);
    let s = await state();
    check(s.source === '' && s.appliedSource === '' && !s.dirty, 'initial editor has literally empty source');
    check(s.views.length === 0 && Object.keys(s.states).length === 0 && s.held.length === 0 && !s.running, 'initial scene has no built-in entities, states, inputs, or running clock');
    check(await page.locator('#input-controls button').count() === 0 && await page.locator('#play').isDisabled(), 'controls require an authored program');
    await page.screenshot({ path: shots[0], fullPage: true });

    await page.locator('#source').fill(authorSource);
    s = await state();
    check(s.dirty && s.views.length === 0 && s.appliedSource === '', 'authoring a new program is a draft until Apply');
    await page.locator('#apply').click();
    s = await state();
    check(!s.dirty && s.appliedSource === authorSource && s.views.length === 2 && s.states.x === 90 && s.states.count === 0, 'new custom program constructs states and multiple views from source');
    check(await page.locator('#input-controls button').count() === 2 && await page.locator('[data-input="advance"]').count() === 1 && await page.locator('[data-input="pulse"]').count() === 1, 'input buttons are generated from the authored declarations');
    check(await page.locator('#scene-content [data-view-id="marker"]').getAttribute('x') === '90', 'view bindings render the authored state value');

    await page.locator('#source').focus();
    await page.keyboard.press('End');
    await page.keyboard.press('Space');
    await page.keyboard.press('ArrowRight');
    check((await state()).states.count === 0 && (await state()).states.moving === 0, 'Space and arrows in the editor never reach authored inputs');
    await page.locator('#source').fill(authorSource);

    const pulse = page.locator('[data-input="pulse"]');
    let box = await pulse.boundingBox();
    await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
    await page.mouse.down();
    s = await state();
    check(s.states.count === 1 && s.held.includes('pulse') && !s.running && s.tick === 0, 'pointer down dispatches once while the clock is paused');
    await page.mouse.up();
    check((await state()).held.length === 0 && (await state()).states.count === 1, 'pointer up releases without double dispatch from click');

    await page.locator('#scene').focus();
    await page.keyboard.down('Space');
    await page.keyboard.down('Space');
    await page.keyboard.up('Space');
    check((await state()).states.count === 2, 'keyboard repeat is ignored');
    await pulse.focus();
    await page.keyboard.press('Enter');
    check((await state()).states.count === 3 && (await state()).held.length === 0, 'keyboard activation on a generated button is one complete tap');
    await pulse.dispatchEvent('click', { detail: 0 });
    check((await state()).states.count === 4 && (await state()).held.length === 0, 'assistive click activation is one complete tap');

    await page.locator('#scene').focus();
    await page.keyboard.down('Space');
    box = await pulse.boundingBox();
    await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
    await page.mouse.down();
    check((await state()).states.count === 5 && (await state()).held.includes('pulse'), 'keyboard and pointer sources share one logical input');
    await page.mouse.up();
    check((await state()).held.includes('pulse'), 'releasing one source does not release the other source');
    await page.keyboard.up('Space');
    check((await state()).held.length === 0, 'last source releases the logical input');

    await page.locator('#scene').focus();
    await page.keyboard.down('ArrowRight');
    await page.locator('#step').click();
    await page.locator('#step').click();
    s = await state();
    check(s.states.x === 94 && s.states.moving === 1 && s.tick === 2, 'authored down/frame behavior uses the declared dt without built-in motion');
    await page.locator('#source').focus();
    await page.keyboard.up('ArrowRight');
    check((await state()).states.moving === 0 && (await state()).held.length === 0, 'keyup releases even after focus moves into the editor');
    await page.locator('#scene').focus();
    await page.keyboard.down('ArrowRight');
    await page.evaluate(() => window.dispatchEvent(new Event('blur')));
    await page.keyboard.up('ArrowRight');
    check((await state()).states.moving === 0 && !(await state()).running && (await state()).held.length === 0, 'focus loss sends release and pauses the clock');

    await page.locator('#play').click();
    await page.waitForFunction(() => window.pureWorkbench.snapshot().tick >= 5);
    await page.locator('#play').click();
    const paused = await state();
    await page.waitForTimeout(100);
    check(!paused.running && paused.tick === (await state()).tick && paused.tick >= 5, 'Play advances and Pause freezes the clock');

    const beforeFailure = await state();
    await apply(authorSource + 'state broken =\n');
    s = await state();
    check(s.appliedSource === authorSource && s.revision === beforeFailure.revision && JSON.stringify(s.states) === JSON.stringify(beforeFailure.states), 'invalid code preserves the previous executable program and states');
    check(await page.locator('#error-location').isVisible() && /행/.test(s.diagnostic) && await page.locator('#source').getAttribute('aria-invalid') === 'true', 'compile error exposes a source-line diagnostic');
    await page.locator('#error-location').click();
    check(await page.locator('#source').evaluate(el => el.selectionStart >= el.value.indexOf('state broken')), 'diagnostic location moves the editor caret to the problem');

    const expanded = authorSource.replace('state x = 90', 'state x = 500').replace('count = count + 1', 'count = count + 5') + [
      'state bonus = 7', 'input reverse = KeyA',
      'circle beacon(300, 190, 15 + bonus, "#c9f08b")',
      'on reverse.down { bonus = bonus + 2 }', '',
    ].join('\n');
    await apply(expanded);
    s = await state();
    check(s.states.x === beforeFailure.states.x && s.states.count === 5 && s.states.bonus === 7 && s.views.length === 3, 'apply preserves matching state names and initializes newly authored states');
    await page.locator('[data-input="reverse"]').click();
    check((await state()).states.bonus === 9 && await page.locator('#scene-content [data-view-id="beacon"]').getAttribute('r') === '24', 'new input, entity, binding, and behavior work with source changes alone');
    await page.locator('[data-input="pulse"]').click();
    check((await state()).states.count === 10, 'changed behavior takes effect without resetting existing values');

    const withDraft = expanded + '# This un-applied draft must survive Reset.\n';
    await page.locator('#source').fill(withDraft);
    await page.locator('#reset').click();
    s = await state();
    check(s.states.x === 500 && s.states.count === 0 && s.states.bonus === 7 && s.tick === 0 && s.source === withDraft && s.dirty, 'explicit Reset restores active initializers and keeps the draft');
    await page.locator('#scene').focus();
    await page.keyboard.down('ArrowRight');
    const heldRevision = (await state()).revision;
    await page.locator('#apply').click();
    check((await state()).revision === heldRevision && /놓은/.test((await state()).diagnostic), 'Apply is refused while an input is held');
    await page.keyboard.up('ArrowRight');

    const downloadPromise = page.waitForEvent('download');
    await page.locator('#download-project').click();
    const projectDownload = await downloadPromise;
    const downloadPath = 'output/playwright/pure-pointer-workbench-draft.json';
    await projectDownload.saveAs(downloadPath);
    downloaded = { filename: projectDownload.suggestedFilename(), path: downloadPath, expectedSource: withDraft };
    await page.locator('#new').click();
    s = await state();
    check(s.source === '' && s.views.length === 3 && s.appliedSource === expanded, 'clear editor is explicit and does not clear the running scene before Apply');
    await page.locator('#file-input').setInputFiles(downloadPath);
    await page.waitForFunction(expected => window.pureWorkbench.snapshot().source === expected, withDraft);
    check((await state()).source === withDraft && (await state()).appliedSource === expanded && (await state()).dirty, 'downloaded project restores the exact un-applied draft without execution');
    await page.locator('#file-input').evaluate(input => {
      const transfer = new DataTransfer();
      transfer.items.add(new File(['{"format":"other","version":1,"source":"state no = 1"}'], 'invalid.json', { type: 'application/json' }));
      input.files = transfer.files;
      input.dispatchEvent(new Event('change', { bubbles: true }));
    });
    await page.waitForFunction(() => window.pureWorkbench.snapshot().diagnostic.includes('형식'));
    check((await state()).source === withDraft && (await state()).appliedSource === expanded, 'invalid JSON project schema cannot replace the draft');

    await apply('state n = 1\ninput fail = Space\ntext label(40, 60, n)\non fail.down { n = n / 0 }\n');
    await page.locator('[data-input="fail"]').dispatchEvent('click', { detail: 0 });
    s = await state();
    check(s.states.n === 1 && s.lastError === 4 && s.held.length === 0 && s.lastEvent.phase === 'up', 'failed assistive down event rolls back and its error survives the following successful up');
    check(await page.locator('#runtime-error').isVisible() && (await page.locator('#runtime-error').textContent()).includes('마지막 실패'), 'latched runtime failure is visible and labeled separately from the latest input');
    await page.locator('#reset').click();
    check((await state()).lastError === 0 && await page.locator('#runtime-error').isHidden(), 'explicit Reset clears the failure record');

    await page.locator('#new').click();
    await page.locator('#apply').click();
    s = await state();
    check(s.source === '' && s.appliedSource === '' && s.views.length === 0 && Object.keys(s.states).length === 0 && await page.locator('#input-controls button').count() === 0, 'applying empty source removes all authored states, inputs, and scene contents');
    await page.locator('#example').selectOption('counter');
    await page.locator('#load-example').click();
    await page.waitForFunction(() => window.pureWorkbench.snapshot().source.includes('state count'));
    check((await state()).views.length === 0 && (await state()).appliedSource === '' && (await state()).dirty, 'explicit example loading only fills the editor and never auto-applies');

    await apply(authorSource);
    await page.locator('[data-input="pulse"]').click();
    await page.locator('[data-input="pulse"]').click();
    await page.locator('#scene').focus();
    await page.keyboard.down('ArrowRight');
    for (let i = 0; i < 12; i++) await page.locator('#step').click();
    await page.keyboard.up('ArrowRight');
    lastAuthored = await state();
    check(lastAuthored.states.x === 114 && lastAuthored.states.count === 2 && lastAuthored.memory.linearBytes === 65536, 'authored scene executes in the actual fixed 64 KiB Wasm memory');
    await page.locator('.cost-details summary').click();
    check((await page.locator('#cost-values').textContent()).includes('65,536 B'), 'cost inspector shows measured Wasm memory without claiming browser heap totals');
    await page.locator('.cost-details summary').click();
    await page.screenshot({ path: shots[1], fullPage: true });

    await page.setViewportSize({ width: 390, height: 844 });
    mobile = await page.evaluate(() => ({ viewport: innerWidth, content: document.documentElement.scrollWidth }));
    check(mobile.content <= mobile.viewport, '390px mobile viewport has no horizontal overflow');
    await page.locator('[data-input="pulse"]').click();
    check((await state()).states.count === 3, 'generated mobile input buttons remain usable');
    await page.screenshot({ path: shots[2], fullPage: true });
    const comparison = await page.request.get('http://127.0.0.1:8766/baseline.html');
    const equivalent = await page.request.get('http://127.0.0.1:8766/examples/charge.pure');
    check(comparison.ok() && equivalent.ok() && await page.locator('a[href="./baseline.html"]').count() === 1, 'ordinary-JavaScript comparison and equivalent Pure example are linked and served');

    await page.route('**/core.wasm', route => route.abort());
    await page.reload();
    await page.waitForFunction(() => document.getElementById('ready-status').textContent.includes('실패'));
    check(await page.locator('#apply').isDisabled() && await page.locator('#play').isDisabled() && await page.locator('#step').isDisabled() && /실행기를 열지/.test(await page.locator('#diagnostic').textContent()), 'missing Wasm shows an actionable load failure and disables execution controls');
    await page.unroute('**/core.wasm');
    await page.setViewportSize({ width: 1440, height: 1000 });
    await page.reload();
    await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready);
    check((await state()).source === '' && (await state()).views.length === 0, 'fresh load always returns to a blank authoring environment');
    check(errors.length === 0, 'no uncaught page errors during the browser scenarios');
    const evidence = { recordedUtc: new Date().toISOString(), browser: await page.evaluate(() => navigator.userAgent),
      checks, authoredSource: authorSource, authoredSnapshot: lastAuthored, mobile, downloaded, errors, screenshots: shots };
    // CLI can yield early when native dialogs/download events occur. Retain the
    // observation report so a follow-up CLI call can retrieve the complete run.
    await page.evaluate(value => { window.__pureWorkbenchQAEvidence = value; }, evidence);
    return evidence;
  } catch (error) {
    const evidence = { failed: true, error: error.message, stack: error.stack, checks, errors };
    await page.evaluate(value => { window.__pureWorkbenchQAEvidence = value; }, evidence);
    return evidence;
  } finally {
    page.off('pageerror', onError);
    page.off('dialog', acceptDialog);
  }
}