aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes/workbench/qa-routed-input.js
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-24 17:14:54 +0100
committereunhhu <[email protected]>2026-09-24 17:14:54 +0100
commitb1ce1927022d374198441afafeb27199fc8a052a (patch)
tree6021f175f4c67ed03b44240eaa2f90d846f12f6f /prototypes/workbench/qa-routed-input.js
parentd34f9426d5317a1948f12f229439946728bc342a (diff)
downloadpure-b1ce1927022d374198441afafeb27199fc8a052a.tar.gz
pure-b1ce1927022d374198441afafeb27199fc8a052a.zip
Establish native micro runtime checkpoint
Diffstat (limited to 'prototypes/workbench/qa-routed-input.js')
-rw-r--r--prototypes/workbench/qa-routed-input.js164
1 files changed, 164 insertions, 0 deletions
diff --git a/prototypes/workbench/qa-routed-input.js b/prototypes/workbench/qa-routed-input.js
new file mode 100644
index 0000000..b935499
--- /dev/null
+++ b/prototypes/workbench/qa-routed-input.js
@@ -0,0 +1,164 @@
+// Functional end-to-end persistence checks. Run with Playwright CLI run-code.
+async page => {
+ const checks=[],errors=[];
+ const check=(ok,label)=>{if(!ok)throw new Error(label);checks.push(label);};
+ const state=()=>page.evaluate(()=>window.pureWorkbench.snapshot());
+ const onDialog=dialog=>dialog.accept(), onError=error=>errors.push(error.message);
+ page.on('dialog',onDialog);page.on('pageerror',onError);
+ const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b);
+ const client=(x,y)=>page.evaluate(({x,y})=>{const p=new DOMPoint(x,y).matrixTransform(document.querySelector('#scene').getScreenCTM());return{x:p.x,y:p.y};},{x,y});
+ const move=async(x,y)=>{const p=await client(x,y);await page.mouse.move(p.x,p.y);};
+ const drag=async(x,y,nx,ny)=>{await page.locator('#scene').scrollIntoViewIfNeeded();await move(x,y);await page.mouse.down();await move(nx,ny);await page.mouse.up();};
+ const upload=async(text,name='layout.json')=>{
+ await page.locator('#file-input').evaluate((input,{text,name})=>{
+ const transfer=new DataTransfer();transfer.items.add(new File([text],name,{type:'application/json'}));
+ input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true}));
+ },{text,name});
+ };
+ try {
+ await page.setViewportSize({width:1440,height:1000});
+ await page.waitForFunction(()=>window.pureWorkbench?.snapshot().ready);
+ check(await page.locator('#download-layout').isDisabled(),'placement save requires an applied definition');
+ await page.locator('#example').selectOption('routed-layout');await page.locator('#load-example').click();await page.locator('#apply').click();
+ const source=await page.locator('#source').inputValue();
+ check(await page.locator('[data-point-input="drag"]').count()===3,'one authored pointer input connects all three view targets');
+ check(await page.locator('[data-point-target]').count()===3,'DOM destinations retain distinct target identities');
+ await drag(90,130,160,180);await drag(290,170,360,220);await drag(460,230,530,270);
+ check((await page.locator('#last-event').textContent()).includes('drag.up → amber_box'),'last input identifies the released target within its group');
+ await page.keyboard.press('ArrowLeft');
+ let s=await state();
+ check(Math.abs(s.states['green.x']-150)<0.1&&Math.abs(s.states['blue.x']-350)<0.1&&Math.abs(s.states['amber.x']-510)<0.1,'three independently authored targets move and selection directs keyboard adjustment');
+ const expected={states:s.states,views:s.views};
+ const draft=source+'\nstate unfinished =';
+ await page.locator('#source').fill(draft);
+ const pending=page.waitForEvent('download');await page.locator('#download-layout').click();
+ const download=await pending;const saved='output/playwright/pure-routed-layout-saved.json';await download.saveAs(saved);
+ check(download.suggestedFilename()==='my-layout.pure.json','placement download is a separate document file');
+ check((await state()).source===draft&&(await state()).appliedSource===source,'saving neither applies nor discards the unfinished draft');
+ await page.locator('#reset').click();
+ check((await state()).states['green.x']===80,'reset returns to authored initial positions');
+ await page.locator('#file-input').setInputFiles(saved);
+ await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원'));
+ s=await state();
+ check(same(s.states,expected.states)&&same(s.views,expected.views),'downloaded file restores all states and visible positions');
+ check(s.source===draft&&s.appliedSource===source&&s.dirty,'the draft remains separate from the restored executable definition');
+ check(!s.running&&!s.held.length&&s.tick===0&&s.successfulEvents===0,'restored work starts stopped with no replayed inputs or clock');
+ await page.locator('#scene').focus();await page.keyboard.press('ArrowRight');
+ check(Math.abs((await state()).states['amber.x']-(expected.states['amber.x']+10))<0.1,'restored selection and bindings remain editable');
+ // Use a fresh page/runtime, not just preservation inside the original instance.
+ const fresh=await page.context().newPage();await fresh.goto(page.url());
+ await fresh.waitForFunction(()=>window.pureWorkbench?.snapshot().ready);
+ await fresh.locator('#file-input').setInputFiles(saved);
+ await fresh.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원'));
+ const reopened=await fresh.evaluate(()=>window.pureWorkbench.snapshot());
+ check(same(reopened.states,expected.states)&&same(reopened.views,expected.views)&&reopened.source===draft,'a fresh browser page reconstructs the saved work without old runtime state');
+ await fresh.close();await page.bringToFront();
+
+ const before=await state();
+ await upload(JSON.stringify({format:'pure-workbench',version:2,source:'state changed=99',draft:'replacement',states:[]}));
+ await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('상태 목록'));
+ s=await state();
+ check(same(s.states,before.states)&&s.source===before.source&&s.appliedSource===before.appliedSource&&s.revision===before.revision,'invalid saved state cannot partially replace code, draft or scene');
+ // Delay File.text, then edit before it completes. Restoring must not overwrite the edit.
+ await page.locator('#file-input').evaluate(input=>{
+ const document={format:'pure-workbench',version:2,source:'state other=4',draft:'state other=4',states:[{name:'other',value:55}]};
+ const transfer=new DataTransfer(),file=new File([JSON.stringify(document)],'delayed.json',{type:'application/json'});
+ const text=JSON.stringify(document);file.text=()=>new Promise(resolve=>{window.__finishLayoutRead=()=>resolve(text);});
+ transfer.items.add(file);input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true}));
+ });
+ await page.locator('#source').fill(draft+'\n# edit during file read');
+ await page.evaluate(()=>window.__finishLayoutRead());
+ await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('작업이 바뀌'));
+ s=await state();check(s.source.endsWith('# edit during file read')&&same(s.states,before.states),'a delayed file read cannot overwrite a newer draft');
+ // Repeat with an input event while file content is pending.
+ await page.locator('#file-input').evaluate(input=>{
+ const text=JSON.stringify({format:'pure-workbench',version:2,source:'state other=4',states:[{name:'other',value:55}]});
+ const file=new File([text],'delayed.json',{type:'application/json'}),transfer=new DataTransfer();
+ file.text=()=>new Promise(resolve=>{window.__finishLayoutRead=()=>resolve(text);});
+ transfer.items.add(file);input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true}));
+ });
+ await page.locator('#scene').focus();await page.keyboard.press('ArrowRight');const changed=await state();
+ await page.evaluate(()=>window.__finishLayoutRead());
+ await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('작업이 바뀌'));
+ check(same((await state()).states,changed.states),'a delayed restore cannot overwrite placement changes made while reading');
+ // UI-only run state is deliberately not persisted.
+ await page.locator('#play').click();await page.locator('#download-layout').click();
+ check((await state()).diagnostic.includes('정지한 뒤'),'saving while time advances requests a stopped document');
+ await page.locator('#play').click();
+ // Refuse while a logical input remains held, with no lost source or partial save.
+ await page.locator('#scene').focus();await page.keyboard.down('ArrowRight');
+ await page.locator('#download-layout').click();
+ check((await state()).diagnostic.includes('조작을 끝낸 뒤'),'saving with a held input is refused');
+ await page.keyboard.up('ArrowRight');
+ await page.locator('#file-input').setInputFiles(saved);
+ await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원'));
+ // One source edit affects every bound target, without editing input adapters.
+ const edited=source.replace('t.x = clamp(t.x + 10,','t.x = clamp(t.x + 25,');
+ await page.locator('#source').fill(edited);await page.locator('#apply').click();
+ const beforeEdit=await state();
+ await page.locator('#scene').focus();await page.keyboard.press('ArrowRight');
+ check(Math.abs((await state()).states['amber.x']-(beforeEdit.states['amber.x']+25))<0.1,'editing the shared rule changes the selected amber target');
+ await drag(beforeEdit.states['blue.x']+10,beforeEdit.states['blue.y']+10,beforeEdit.states['blue.x']+10,beforeEdit.states['blue.y']+10);
+ const blueBefore=(await state()).states['blue.x'];await page.keyboard.press('ArrowRight');
+ check(Math.abs((await state()).states['blue.x']-(blueBefore+25))<0.1,'the same single rule edit also changes the blue target');
+ const lines=edited.split('\n'),items=lines.filter(line=>line.startsWith('item ')).reverse();
+ const reordered=lines.map(line=>line.startsWith('item ')?items.shift():line).join('\n');
+ const beforeReorder=await state();await page.locator('#source').fill(reordered);await page.locator('#apply').click();
+ const afterReorder=await state();
+ check(Object.entries(beforeReorder.states).every(([name,value])=>afterReorder.states[name]===value),'reordering instance declarations preserves every field by qualified name');
+ await page.locator('#scene').focus();await page.keyboard.press('ArrowRight');
+ check(Math.abs((await state()).states['blue.x']-(afterReorder.states['blue.x']+25))<0.1,'rebuilt parameter maps still send selection input to blue after reorder');
+ // Two contacts execute the same body with different bound fields.
+ await page.locator('#source').fill(source);await page.locator('#apply').click();await page.locator('#reset').click();
+ await page.locator('#scene').scrollIntoViewIfNeeded();
+ const session=await page.context().newCDPSession(page);
+ try {
+ await session.send('Emulation.setTouchEmulationEnabled',{enabled:true,maxTouchPoints:2});
+ const p1={id:1,...await client(90,130)},p2={id:2,...await client(290,170)};
+ await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1]});
+ await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1,p2]});
+ await session.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[{id:1,...await client(160,180)},{id:2,...await client(360,220)}]});
+ await page.waitForFunction(()=>{const s=window.pureWorkbench.snapshot().states;return Math.abs(s['green.x']-150)<0.1&&Math.abs(s['blue.x']-350)<0.1;});
+ const touch=await state();
+ check(Math.abs(touch.states['green.x']-150)<0.1&&Math.abs(touch.states['blue.x']-350)<0.1&&touch.held.length===2,'simultaneous touches share code but retain independent field bindings');
+ await session.send('Input.dispatchTouchEvent',{type:'touchCancel',touchPoints:[]});
+ const canceled=await state();
+ check(canceled.states['green.x']===80&&canceled.states['blue.x']===280&&!canceled.held.length,'shared cancel restores each contact to its own start position');
+
+ // A real second contact on the same view must not steal its owner.
+ await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1]});
+ await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1,{id:2,...await client(100,140)}]});
+ check((await state()).held.length===1,'a second touch on the same target cannot steal ownership');
+ await session.send('Input.dispatchTouchEvent',{type:'touchCancel',touchPoints:[]});
+ // End one actual contact and continue the other in the same logical input.
+ await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1]});
+ await session.send('Input.dispatchTouchEvent',{type:'touchStart',touchPoints:[p1,p2]});
+ const m1={id:1,...await client(160,180)},m2={id:2,...await client(360,220)};
+ await session.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[m1,m2]});
+ // In this Chromium, touchEnd's supplied point identifies the contact ending.
+ // touch-reproduction.js records the resulting pointerup and retained blue owner.
+ await session.send('Input.dispatchTouchEvent',{type:'touchEnd',touchPoints:[m1]});
+ let partial=await state();
+ check(partial.held.length===1&&partial.states['green.held']===0&&partial.states['blue.held']===1,'releasing green keeps blue captured within the same input');
+ await session.send('Input.dispatchTouchEvent',{type:'touchMove',touchPoints:[{id:2,...await client(400,250)}]});
+ await page.waitForFunction(()=>Math.abs(window.pureWorkbench.snapshot().states['blue.x']-390)<0.1);
+ partial=await state();
+ check(Math.abs(partial.states['green.x']-150)<0.1&&Math.abs(partial.states['blue.x']-390)<0.1,'the remaining contact keeps moving only its own target');
+ await session.send('Input.dispatchTouchEvent',{type:'touchCancel',touchPoints:[]});
+ partial=await state();
+ check(Math.abs(partial.states['green.x']-150)<0.1&&partial.states['blue.x']===280&&!partial.held.length,'canceling the remaining target preserves the already released position');
+ } finally {await session.send('Emulation.setTouchEmulationEnabled',{enabled:false});await session.detach();}
+ await page.locator('#file-input').setInputFiles(saved);
+ await page.waitForFunction(()=>window.pureWorkbench.snapshot().diagnostic.includes('배치를 복원'));
+ await page.screenshot({path:'output/playwright/pure-routed-layout-desktop.png',fullPage:true});
+ await page.setViewportSize({width:390,height:844});
+ check(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),'extra persistence control fits a mobile viewport');
+ await page.screenshot({path:'output/playwright/pure-routed-layout-mobile.png',fullPage:true});
+ check(errors.length===0,'no uncaught page errors');
+ const result={checks,errors,saved,expected,source,draft};
+ await page.evaluate(result=>{window.__pureRoutedInputEvidence=result;},result);
+ return {checks:checks.length,errors};
+ } catch(error) {
+ await page.evaluate(result=>{window.__pureRoutedInputEvidence=result;},{failed:String(error),checks,errors});throw error;
+ } finally {page.off('dialog',onDialog);page.off('pageerror',onError);}
+}