Skip to content

Commit c6c0866

Browse files
sudomakeinstallfinetjul
authored andcommitted
test: Add tests for chorded mouse button handling
1 parent f7c7924 commit c6c0866

1 file changed

Lines changed: 243 additions & 0 deletions

File tree

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import test from 'tape';
2+
3+
import vtkRenderWindowInteractor from 'vtk.js/Sources/Rendering/Core/RenderWindowInteractor';
4+
5+
// Helpers ----------------------------------------------------------------
6+
7+
function makePointerEvent(type, { button = 0, buttons = 0 } = {}) {
8+
return new PointerEvent(type, {
9+
bubbles: true,
10+
pointerId: 1,
11+
pointerType: 'mouse',
12+
button,
13+
buttons,
14+
clientX: 100,
15+
clientY: 100,
16+
});
17+
}
18+
19+
function setupInteractor() {
20+
const container = document.createElement('div');
21+
Object.defineProperty(container, 'clientWidth', { value: 400 });
22+
Object.defineProperty(container, 'clientHeight', { value: 400 });
23+
// Stub pointer capture methods — synthetic PointerEvents have no real
24+
// active pointer, so the native methods throw NotFoundError.
25+
container.setPointerCapture = () => {};
26+
container.releasePointerCapture = () => {};
27+
container.hasPointerCapture = () => false;
28+
document.body.appendChild(container);
29+
30+
// Create a minimal mock renderer so that events are not silently dropped
31+
// by the enabled/renderer checks in the interactor event pipeline.
32+
const mockRenderer = { getLayer: () => 0, getInteractive: () => true };
33+
const interactor = vtkRenderWindowInteractor.newInstance({
34+
_forcedRenderer: true,
35+
currentRenderer: mockRenderer,
36+
_getScreenEventPositionFor: (source) => ({
37+
x: source.clientX,
38+
y: source.clientY,
39+
z: 0,
40+
movementX: source.movementX || 0,
41+
movementY: source.movementY || 0,
42+
}),
43+
});
44+
interactor.setContainer(container);
45+
interactor.initialize();
46+
47+
return { container, interactor };
48+
}
49+
50+
function teardown({ container, interactor }) {
51+
interactor.setContainer(null);
52+
interactor.delete();
53+
container.remove();
54+
}
55+
56+
// Tests ------------------------------------------------------------------
57+
58+
test('Test RenderWindowInteractor chorded button press', (t) => {
59+
const env = setupInteractor();
60+
const { container, interactor } = env;
61+
62+
const events = [];
63+
const sub1 = interactor.onLeftButtonPress(() =>
64+
events.push('LeftButtonPress')
65+
);
66+
const sub2 = interactor.onRightButtonPress(() =>
67+
events.push('RightButtonPress')
68+
);
69+
const sub3 = interactor.onLeftButtonRelease(() =>
70+
events.push('LeftButtonRelease')
71+
);
72+
const sub4 = interactor.onRightButtonRelease(() =>
73+
events.push('RightButtonRelease')
74+
);
75+
76+
// 1. Press left button (pointerdown fires for first button)
77+
container.dispatchEvent(
78+
makePointerEvent('pointerdown', { button: 0, buttons: 1 })
79+
);
80+
t.deepEqual(events, ['LeftButtonPress'], 'Left press registered');
81+
82+
// 2. Press right while left held (pointermove with button change per spec §10)
83+
container.dispatchEvent(
84+
makePointerEvent('pointermove', { button: 2, buttons: 3 })
85+
);
86+
t.ok(
87+
events.includes('RightButtonPress'),
88+
'Chorded right press detected via pointermove'
89+
);
90+
91+
// 3. Release left while right held (pointermove with button change)
92+
events.length = 0;
93+
container.dispatchEvent(
94+
makePointerEvent('pointermove', { button: 0, buttons: 2 })
95+
);
96+
t.deepEqual(
97+
events,
98+
['LeftButtonRelease'],
99+
'Chorded left release detected via pointermove'
100+
);
101+
102+
// 4. Release right - last button (pointerup fires)
103+
events.length = 0;
104+
container.dispatchEvent(
105+
makePointerEvent('pointerup', { button: 2, buttons: 0 })
106+
);
107+
t.deepEqual(events, ['RightButtonRelease'], 'Right release registered');
108+
109+
sub1.unsubscribe();
110+
sub2.unsubscribe();
111+
sub3.unsubscribe();
112+
sub4.unsubscribe();
113+
teardown(env);
114+
t.end();
115+
});
116+
117+
test('Test RenderWindowInteractor single button (no false chorded events)', (t) => {
118+
const env = setupInteractor();
119+
const { container, interactor } = env;
120+
121+
const events = [];
122+
const sub1 = interactor.onLeftButtonPress(() =>
123+
events.push('LeftButtonPress')
124+
);
125+
const sub2 = interactor.onLeftButtonRelease(() =>
126+
events.push('LeftButtonRelease')
127+
);
128+
const sub3 = interactor.onRightButtonPress(() =>
129+
events.push('RightButtonPress')
130+
);
131+
const sub4 = interactor.onRightButtonRelease(() =>
132+
events.push('RightButtonRelease')
133+
);
134+
135+
// Normal left click cycle
136+
container.dispatchEvent(
137+
makePointerEvent('pointerdown', { button: 0, buttons: 1 })
138+
);
139+
container.dispatchEvent(
140+
makePointerEvent('pointermove', { button: -1, buttons: 1 })
141+
);
142+
container.dispatchEvent(
143+
makePointerEvent('pointerup', { button: 0, buttons: 0 })
144+
);
145+
146+
t.deepEqual(
147+
events,
148+
['LeftButtonPress', 'LeftButtonRelease'],
149+
'Simple left click produces no spurious chorded events'
150+
);
151+
152+
sub1.unsubscribe();
153+
sub2.unsubscribe();
154+
sub3.unsubscribe();
155+
sub4.unsubscribe();
156+
teardown(env);
157+
t.end();
158+
});
159+
160+
test('Test RenderWindowInteractor three-button chord', (t) => {
161+
const env = setupInteractor();
162+
const { container, interactor } = env;
163+
164+
const events = [];
165+
const subs = [
166+
interactor.onLeftButtonPress(() => events.push('LP')),
167+
interactor.onMiddleButtonPress(() => events.push('MP')),
168+
interactor.onRightButtonPress(() => events.push('RP')),
169+
interactor.onLeftButtonRelease(() => events.push('LR')),
170+
interactor.onMiddleButtonRelease(() => events.push('MR')),
171+
interactor.onRightButtonRelease(() => events.push('RR')),
172+
];
173+
174+
// Press left
175+
container.dispatchEvent(
176+
makePointerEvent('pointerdown', { button: 0, buttons: 1 })
177+
);
178+
// Press middle (chorded)
179+
container.dispatchEvent(
180+
makePointerEvent('pointermove', { button: 1, buttons: 5 })
181+
);
182+
// Press right (chorded)
183+
container.dispatchEvent(
184+
makePointerEvent('pointermove', { button: 2, buttons: 7 })
185+
);
186+
// Release all three simultaneously (only pointerup fires with buttons=0)
187+
container.dispatchEvent(
188+
makePointerEvent('pointerup', { button: 0, buttons: 0 })
189+
);
190+
191+
// Chorded releases (middle, right) fire before the primary button release
192+
// (left) which is handled by handleMouseUp.
193+
t.deepEqual(
194+
events,
195+
['LP', 'MP', 'RP', 'MR', 'RR', 'LR'],
196+
'All presses and releases detected in deterministic order'
197+
);
198+
199+
subs.forEach((s) => s.unsubscribe());
200+
teardown(env);
201+
t.end();
202+
});
203+
204+
test('Test RenderWindowInteractor pointercancel releases all held buttons', (t) => {
205+
const env = setupInteractor();
206+
const { container, interactor } = env;
207+
208+
const events = [];
209+
const subs = [
210+
interactor.onLeftButtonPress(() => events.push('LP')),
211+
interactor.onMiddleButtonPress(() => events.push('MP')),
212+
interactor.onRightButtonPress(() => events.push('RP')),
213+
interactor.onLeftButtonRelease(() => events.push('LR')),
214+
interactor.onMiddleButtonRelease(() => events.push('MR')),
215+
interactor.onRightButtonRelease(() => events.push('RR')),
216+
];
217+
218+
// Press left
219+
container.dispatchEvent(
220+
makePointerEvent('pointerdown', { button: 0, buttons: 1 })
221+
);
222+
// Press middle (chorded)
223+
container.dispatchEvent(
224+
makePointerEvent('pointermove', { button: 1, buttons: 5 })
225+
);
226+
227+
events.length = 0;
228+
229+
// Cancel the interaction — all held buttons should be released
230+
container.dispatchEvent(
231+
makePointerEvent('pointercancel', { button: 0, buttons: 0 })
232+
);
233+
234+
t.deepEqual(
235+
events,
236+
['LR', 'MR'],
237+
'pointercancel fires release events for all held buttons'
238+
);
239+
240+
subs.forEach((s) => s.unsubscribe());
241+
teardown(env);
242+
t.end();
243+
});

0 commit comments

Comments
 (0)