diff options
Diffstat (limited to 'prototypes/workbench/qa-presentation.js')
| -rw-r--r-- | prototypes/workbench/qa-presentation.js | 141 |
1 files changed, 141 insertions, 0 deletions
diff --git a/prototypes/workbench/qa-presentation.js b/prototypes/workbench/qa-presentation.js new file mode 100644 index 0000000..90595e3 --- /dev/null +++ b/prototypes/workbench/qa-presentation.js @@ -0,0 +1,141 @@ +// Playwright CLI run-code function. The Python runner injects the archived +// app/runtime source at BASELINE_SOURCES; production files are never replaced. +async (page) => { + const baseline = /* BASELINE_SOURCES */ null; + const checks = [], errors = [], samples = []; + const check = (ok, name) => { if (!ok) throw new Error(name); checks.push(name); }; + const onError = error => errors.push(error.message); + const onDialog = dialog => dialog.accept(); + page.on('pageerror', onError); page.on('dialog', onDialog); + let variant = 'after'; + const source = [ + 'state x = 40', 'state radius = 12', 'input move = KeyM', 'input shrink = KeyS', + 'box moving(x, 100, 30, 30, "#7ed9ae")', 'text label(20, 40, x, "x: ")', + 'circle bubble(500, 150, radius, "#c9f08b")', + ...Array.from({length:21}, (_, i) => `box static${i}(${20 + (i % 7) * 70}, ${200 + Math.floor(i / 7) * 40}, 20, 20, "#8da5bb")`), + 'on move.down { x = x + 0.25 }', 'on shrink.down { radius = radius - 24 }', + ].join('\n'); + const apply = async value => { await page.locator('#source').fill(value); await page.locator('#apply').click(); }; + const state = () => page.evaluate(() => window.pureWorkbench.snapshot()); + const reload = async () => { + await page.reload(); + await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready); + await apply(source); + }; + const routeApp = async route => { + const response = await route.fetch(); + await route.fulfill({response, body: variant === 'before' ? baseline.app : await response.text()}); + }; + const routeRuntime = async route => { + const response = await route.fetch(); + const body = variant === 'before' ? baseline.runtime : await response.text(); + if (!body.includes('snapshot() {')) throw new Error('snapshot instrumentation does not match'); + await route.fulfill({response, body:body.replace('snapshot() {', 'snapshot() { globalThis.__snapshotCalls = (globalThis.__snapshotCalls || 0) + 1;')}); + }; + try { + if (!baseline) throw new Error('Run through qa-presentation.py to supply the archived baseline.'); + await page.route('**/app.js', routeApp); + await page.route('**/runtime.mjs', routeRuntime); + await reload(); + await page.locator('#scene').focus(); + await page.keyboard.press('KeyM'); + check((await state()).states.x === 40.25, 'real keyboard event reaches the authored output'); + await page.evaluate(() => { window.__retained = document.querySelector('[data-view-id="moving"]'); }); + await page.locator('[data-input="move"]').click(); + check((await state()).states.x === 40.5 && await page.locator('[data-view-id="moving"]').getAttribute('x') === '40.5', + 'real pointer input and keyboard input use the same authored behavior'); + check(await page.evaluate(() => window.__retained === document.querySelector('[data-view-id="moving"]')), + 'the displayed element survives an input update'); + await apply(source + '\nstate broken ='); + check((await state()).states.x === 40.5 && await page.evaluate(() => window.__retained === document.querySelector('[data-view-id="moving"]')), + 'failed editing preserves active values and output destinations'); + await page.locator('[data-input="shrink"]').click(); + check((await state()).lastError === 100 && await page.locator('[data-view-id="bubble"]').getAttribute('display') === 'none' && + await page.locator('[data-view-id="moving"]').getAttribute('display') === null, + 'invalid view is hidden while valid views remain published'); + await page.locator('#reset').click(); + check((await state()).lastError === 0 && await page.locator('[data-view-id="bubble"]').getAttribute('display') === null, + 'reset recovers a hidden destination without recreating the scene'); + await apply(source.replace('box moving(x, 100, 30, 30, "#7ed9ae")', 'circle moving(x, 100, 15, "#7ed9ae")')); + check(await page.locator('[data-view-id="moving"]').evaluate(node => node.localName) === 'circle', + 'successful editing replaces changed output structure'); + await apply(''); + check(await page.locator('#scene-content > *').count() === 0 && (await state()).views.length === 0, + 'applying empty work removes all old destinations'); + + // Alternating whole-page runs avoid sharing a runtime or retained DOM across + // variants. Timings cover synchronous dispatch through DOM submission, not + // OS event arrival, layout, paint, or actual display scanout. + for (const mode of ['before', 'after', 'after', 'before', 'before', 'after']) { + variant = mode; + await reload(); + await page.locator('#scene').focus(); + const result = await page.evaluate(() => { + const scene = document.getElementById('scene'); + const tap = () => { + scene.dispatchEvent(new KeyboardEvent('keydown', {code:'KeyM', bubbles:true})); + scene.dispatchEvent(new KeyboardEvent('keyup', {code:'KeyM', bubbles:true})); + }; + for (let i = 0; i < 20; i++) tap(); + const retained = [...document.querySelectorAll('#scene-content > *')]; + const scopes = ['scene-content', 'state-values', 'cost-values']; + const observers = scopes.map(id => { + const observer = new MutationObserver(() => {}); + observer.observe(document.getElementById(id), {subtree:true, childList:true, attributes:true, characterData:true}); + return observer; + }); + const createElement = document.createElement, createElementNS = document.createElementNS; + let createdElements = 0; + document.createElement = function(...args) { createdElements++; return createElement.apply(this, args); }; + document.createElementNS = function(...args) { createdElements++; return createElementNS.apply(this, args); }; + window.__snapshotCalls = 0; + let elapsedMs; + try { + const start = performance.now(); + for (let i = 0; i < 100; i++) tap(); + elapsedMs = performance.now() - start; + } finally { + document.createElement = createElement; document.createElementNS = createElementNS; + } + const snapshotCalls = window.__snapshotCalls; + const mutations = Object.fromEntries(observers.map((observer, i) => { + const records = observer.takeRecords(); observer.disconnect(); + return [scopes[i], {records:records.length, + childList:records.filter(record => record.type === 'childList').length, + attributes:records.filter(record => record.type === 'attributes').length}]; + })); + const nodes = [...document.querySelectorAll('#scene-content > *')]; + const identityPreserved = nodes.every((node, i) => node === retained[i]); + const final = window.pureWorkbench.snapshot(); // outside timed/count interval + return {elapsedMs, createdElements, snapshotCalls, mutations, identityPreserved, + final: {states:final.states, views:final.views, successfulEvents:final.successfulEvents}, + scene:document.getElementById('scene-content').outerHTML}; + }); + samples.push({variant:mode, taps:100, events:200, ...result}); + } + check(samples.every(sample => JSON.stringify(sample.final) === JSON.stringify(samples[0].final)), + 'all six archived/current runs have identical final authored state and views'); + check(samples.filter(s => s.variant === 'after').every(s => s.createdElements === 0 && s.snapshotCalls === 0 && s.identityPreserved), + 'current hot path creates no DOM elements and no full snapshots'); + check(samples.filter(s => s.variant === 'before').every(s => s.createdElements > 0 && s.snapshotCalls === 200 && !s.identityPreserved), + 'archived hot path reproduces full snapshots and element replacement'); + check(samples.filter(s => s.variant === 'after').every(s => s.mutations['scene-content'].attributes === 100 && s.mutations['scene-content'].childList === 100), + 'each tap changes only the moving x attribute and label text in the scene'); + variant = 'after'; await reload(); + await page.screenshot({path:'output/playwright/pure-presentation.png', fullPage:true}); + check(errors.length === 0, 'no uncaught browser errors'); + const evidence = {utc:new Date().toISOString(), checks, errors, samples, source, + timingScope:'100 synchronous synthetic keyboard taps after 20 warm-up taps; JS dispatch to DOM submission only, including instrumentation; not paint/input latency.', + setup:'Archived app/runtime are supplied by test-only routing; snapshot calls and element creation are instrumented. Same Wasm/compiler and production configuration.', + browser:await page.evaluate(() => navigator.userAgent), screenshot:'output/playwright/pure-presentation.png'}; + await page.evaluate(value => { window.__purePresentationEvidence = value; }, evidence); + return evidence; + } catch (error) { + const evidence = {failed:true, message:error.stack || error.message, checks, errors, samples}; + await page.evaluate(value => { window.__purePresentationEvidence = value; }, evidence); + return evidence; + } finally { + await page.unroute('**/app.js', routeApp); await page.unroute('**/runtime.mjs', routeRuntime); + page.off('pageerror', onError); page.off('dialog', onDialog); + } +} |
