aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes/workbench/pointer.test.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'prototypes/workbench/pointer.test.mjs')
-rw-r--r--prototypes/workbench/pointer.test.mjs118
1 files changed, 118 insertions, 0 deletions
diff --git a/prototypes/workbench/pointer.test.mjs b/prototypes/workbench/pointer.test.mjs
new file mode 100644
index 0000000..8bf530d
--- /dev/null
+++ b/prototypes/workbench/pointer.test.mjs
@@ -0,0 +1,118 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { readFile } from 'node:fs/promises';
+import { compile } from './web/compiler.mjs';
+import { createRuntime } from './web/runtime.mjs';
+
+const wasm = await readFile(new URL('./web/core.wasm', import.meta.url));
+const source = await readFile(new URL('./examples/direct.pure', import.meta.url), 'utf8');
+const fresh = async () => { const r = await createRuntime(wasm, {cacheEntries:32}); r.apply(source); return r; };
+
+test('pointer declarations resolve forward targets and reject ambiguous or contextless inputs', () => {
+ const p = compile(source);
+ assert.equal(p.inputs[0].target, 'tile');
+ assert(p.handlers.some(h => h.event === 17)); assert(p.handlers.some(h => h.event === 25));
+ assert(p.instructions.some(i => i.op === 22)); assert(p.instructions.some(i => i.op === 23));
+ for (const [code, message] of [
+ ['input p = pointer(missing)', /Unknown pointer target/],
+ ['input p=pointer(tile)\ninput q=pointer(tile)\ntext tile(0,0,0)', /Duplicate pointer/],
+ ['input p=Space\non p.move {}', /require a pointer/],
+ ['input p=Space\non p.cancel {}', /require a pointer/],
+ ['state x=0\non frame { x=event_x }', /only available/],
+ ['text v(event_y,0,0)', /only available/],
+ ['state event_x=0', /Reserved name/],
+ ]) assert.throws(() => compile(code), message);
+ const full = compile(Array.from({length:8}, (_, i) => `input p${i}=pointer(v${i})\ntext v${i}(0,0,0)\non p${i}.move {}\non p${i}.cancel {}`).join('\n'));
+ assert.deepEqual(full.handlers.map(h => h.event), Array.from({length:8}, (_, i) => [17+i,25+i]).flat());
+});
+
+test('authored drag keeps grab offset and commits the displayed position even if release coordinates differ', async () => {
+ const r = await fresh();
+ r.point('drag', 'down', 90, 170, 'pen');
+ assert.equal(r.snapshot().states.dragging, 1);
+ assert.throws(() => r.apply(''), /놓은 뒤/);
+ r.point('drag', 'move', 360, 220, 'pen');
+ let s = r.snapshot();
+ assert.equal(s.states.x, 350); assert.equal(s.states.y, 210);
+ assert.equal(s.views.find(v => v.id === 'region').value, 1);
+ r.point('drag', 'up', 410, 240, 'pen');
+ s = r.snapshot();
+ assert.equal(s.states.x, 350); assert.equal(s.states.y, 210);
+ assert.equal(s.states.dragging, 0); assert.deepEqual(s.held, []);
+ assert.equal(s.successfulEvents, 3);
+ assert.equal(s.lastEvent.x, 410);
+ r.down('left'); r.up('left');
+ assert.equal(r.snapshot().states.x, 340);
+ r.point('drag', 'down', 350, 220, 'pen');
+ r.point('drag', 'move', 450, 250, 'pen');
+ r.point('drag', 'up', 350, 220, 'pen'); // A host may repeat the press coordinates.
+ assert.equal(r.snapshot().states.x, 440);
+ assert.equal(r.snapshot().states.y, 240);
+ r.apply('state x=0\nstate y=0\ninput p=pointer(tile)\ntext tile(0,0,x)\non p.up { x=event_x\ny=event_y }');
+ r.point('p','down',1,2); r.point('p','up',30,40);
+ assert.deepEqual(r.snapshot().states, {x:30,y:40}); // Payload remains available to authors.
+});
+
+test('ownership, duplicate downs, stale moves, cancellation and focus cleanup cannot leave held input', async () => {
+ const r = await fresh();
+ r.point('drag', 'down', 90, 170, 'a');
+ r.point('drag', 'down', 500, 500, 'a');
+ assert.throws(() => r.point('drag', 'down', 90, 170, 'b'), /다른 포인터/);
+ r.point('drag', 'move', 400, 200, 'b');
+ assert.equal(r.snapshot().successfulEvents, 1);
+ r.point('drag', 'move', 400, 200, 'a');
+ r.releaseAll();
+ const canceled = r.snapshot();
+ assert.equal(canceled.states.x, 80); assert.equal(canceled.states.y, 160);
+ assert.equal(canceled.states.dragging, 0); assert.equal(canceled.lastEvent.phase, 'cancel');
+ assert.deepEqual(canceled.held, []);
+ r.point('drag', 'cancel', 1, 1, 'a'); r.point('drag', 'move', 500, 500, 'a'); r.releaseAll();
+ assert.deepEqual(r.snapshot(), canceled);
+ r.point('drag', 'down', 90, 170, 'new'); r.reset();
+ r.point('drag', 'up', 600, 500, 'new');
+ assert.equal(r.snapshot().states.x, 80); assert.deepEqual(r.snapshot().held, []);
+});
+
+test('invalid coordinates and modes do not mutate a held gesture; arithmetic failure rolls back one event', async () => {
+ const r = await fresh(); r.point('drag', 'down', 90, 170);
+ const before = r.snapshot();
+ for (const invoke of [() => r.point('drag','move',NaN,1), () => r.point('drag','up',1,Infinity),
+ () => r.point('drag','unknown',1,2), () => r.point('left','down',1,2),
+ () => r.down('drag'), () => r.up('drag'), () => r.point('drag','move',1,2,'')]) assert.throws(invoke);
+ assert.deepEqual(r.snapshot(), before);
+ r.releaseAll();
+ r.apply(source.replace('x = clamp(event_x - grab_x, 20, 564)', 'x = event_x\n y = 1 / 0'));
+ r.point('drag', 'down', 90, 170);
+ assert.equal(r.point('drag', 'move', 500, 220), 4);
+ let s = r.snapshot(); assert.equal(s.states.x, 80); assert.equal(s.states.y, 160);
+ assert.equal(s.lastFailure.phase, 'move');
+ r.releaseAll(); s = r.snapshot(); assert.deepEqual(s.held, []); assert.equal(s.lastError, 4);
+ assert.equal(s.states.dragging, 0);
+});
+
+test('independent targets share the event pipeline and publication remains non-reentrant', async () => {
+ const r = await createRuntime(wasm);
+ r.apply('state x=0\nstate y=0\ninput a=pointer(one)\ninput b=pointer(two)\ntext one(0,0,x)\ntext two(0,0,y)\non a.move { x=event_x }\non b.move { y=event_y }');
+ r.point('a','down',1,2,'first'); r.point('b','down',3,4,'second');
+ r.point('a','move',10,20,'first'); r.point('b','move',30,40,'second');
+ assert.deepEqual(r.snapshot().states,{x:10,y:40});
+ r.present({begin(){assert.throws(() => r.point('a','move',0,0,'first'), /출력 전달 중/);},state(){},view(){},value(){},end(){}});
+ r.releaseAll(); assert.deepEqual(r.snapshot().held, []);
+});
+
+test('failing down, up and cancel never prevent terminal ownership cleanup', async () => {
+ for (const phase of ['down', 'up', 'cancel']) {
+ const r = await createRuntime(wasm);
+ r.apply(`state x=7\ninput p=pointer(tile)\ntext tile(0,0,x)\non p.${phase} { x=event_x\nx=1/0 }`);
+ const code = r.point('p','down',10,20,'owner');
+ assert.equal(code, phase === 'down' ? 4 : 0);
+ assert.deepEqual(r.snapshot().held, ['p']);
+ if (phase === 'up') assert.equal(r.point('p','up',30,40,'owner'), 4);
+ else assert.equal(r.releaseAll(), phase === 'cancel' ? 4 : 0);
+ const s = r.snapshot();
+ assert.equal(s.states.x, 7); assert.equal(s.lastError, 4);
+ assert.equal(s.lastFailure.phase, phase); assert.deepEqual(s.held, []);
+ r.apply('state x=100'); // Cleanup also unblocks definition changes.
+ assert.equal(r.snapshot().states.x, 7);
+ }
+});