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
|
// Playwright CLI run-code. Covers release routing and stale release coordinates.
async page => {
const checks = [];
const state = () => page.evaluate(() => window.pureWorkbench.snapshot());
const check = (ok, label) => checks.push({label, passed:Boolean(ok)});
const near = (a,b) => Math.abs(a-b)<0.1;
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 start = async() => {
await page.locator('#reset').click(); await page.locator('#scene').scrollIntoViewIfNeeded();
await move(90,170); await page.mouse.down(); await move(370,230);
};
await page.setViewportSize({width:1440,height:1000});
await page.waitForFunction(() => window.pureWorkbench?.snapshot().ready);
await page.locator('#example').selectOption('direct');
await page.locator('#load-example').click(); await page.locator('#apply').click();
await page.evaluate(() => document.querySelector('#scene').addEventListener('pointerdown',e=>{ window.__releaseID=e.pointerId; }));
await start(); await page.mouse.up(); let s=await state();
check(near(s.states.x,360)&&near(s.states.y,220)&&!s.held.length&&s.lastEvent.phase==='up','ordinary in-scene release retains the displayed position');
await start(); await move(750,400); await page.mouse.up(); s=await state();
check(near(s.states.x,564)&&near(s.states.y,276)&&!s.held.length&&s.lastEvent.phase==='up','ordinary release outside the scene retains the clamped position');
for (const scenario of ['document-pointerup','child-nonbubbling-up','mouse-only-up','stale-scene-up']) {
await start();
const position=await client(90,170); // Deliberately stale press coordinates.
await page.evaluate(({scenario,position})=>{
const scene=document.querySelector('#scene'), tile=document.querySelector('[data-point-input="drag"]');
const fields={pointerId:window.__releaseID,pointerType:'mouse',button:0,buttons:0,clientX:position.x,clientY:position.y,bubbles:scenario!=='child-nonbubbling-up'};
if(scenario==='mouse-only-up') window.dispatchEvent(new MouseEvent('mouseup',fields));
else (scenario==='document-pointerup'?document:scenario==='child-nonbubbling-up'?tile:scene).dispatchEvent(new PointerEvent('pointerup',fields));
if(scene.hasPointerCapture(window.__releaseID)) scene.releasePointerCapture(window.__releaseID);
},{scenario,position});
await page.mouse.up(); s=await state();
check(near(s.states.x,360)&&near(s.states.y,220)&&!s.held.length&&s.lastEvent.phase==='up',`${scenario}: release commits once without a coordinate jump or later cancel`);
check(s.successfulEvents===3,`${scenario}: duplicate release/capture loss produces no extra authored event`);
}
await start();
await page.evaluate(()=>document.querySelector('[data-point-input="drag"]').dispatchEvent(new PointerEvent('lostpointercapture',{pointerId:window.__releaseID,bubbles:true})));
s=await state(); check(s.held.includes('drag')&&near(s.states.x,360),'descendant capture-loss notification cannot cancel the root owner');
await page.mouse.up();
await start();
await page.evaluate(()=>document.querySelector('#scene').releasePointerCapture(window.__releaseID));
await move(380,240); s=await state();
check(!s.held.length&&s.states.x===80&&s.states.y===160&&s.lastEvent.phase==='cancel','actual root capture loss while held still cancels');
await page.mouse.up();
await start();
await page.evaluate(()=>document.dispatchEvent(new PointerEvent('pointercancel',{pointerId:window.__releaseID,bubbles:true})));
await page.mouse.up(); s=await state();
check(!s.held.length&&s.states.x===80&&s.states.y===160&&s.lastEvent.phase==='cancel','cancellation outside the scene still rolls back and ignores the late up');
await start();
await page.evaluate(()=>window.dispatchEvent(new Event('blur')));
await page.mouse.up(); s=await state();
check(!s.held.length&&s.states.x===80&&s.states.y===160&&s.lastEvent.phase==='cancel','focus loss still cancels rather than committing');
const result={checks,passed:checks.filter(c=>c.passed).length,failed:checks.filter(c=>!c.passed)};
await page.evaluate(result=>{window.__pureReleaseEvidence=result;},result);
return result;
}
|