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
|
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);
}
});
|