@forgeax/engine-input 0.0.0-dev.8d955ade1c79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +312 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/browser-backend-dual-instance.test.d.ts +2 -0
- package/dist/__tests__/browser-backend-dual-instance.test.d.ts.map +1 -0
- package/dist/__tests__/browser-backend-focus-loss.test.d.ts +2 -0
- package/dist/__tests__/browser-backend-focus-loss.test.d.ts.map +1 -0
- package/dist/__tests__/browser-backend-lock-error.test.d.ts +2 -0
- package/dist/__tests__/browser-backend-lock-error.test.d.ts.map +1 -0
- package/dist/__tests__/browser-backend-lock-gate.test.d.ts +2 -0
- package/dist/__tests__/browser-backend-lock-gate.test.d.ts.map +1 -0
- package/dist/__tests__/browser-backend-lock-release.test.d.ts +2 -0
- package/dist/__tests__/browser-backend-lock-release.test.d.ts.map +1 -0
- package/dist/__tests__/browser-backend-lock-state.test.d.ts +2 -0
- package/dist/__tests__/browser-backend-lock-state.test.d.ts.map +1 -0
- package/dist/__tests__/canvas-input-boundary.test.d.ts +2 -0
- package/dist/__tests__/canvas-input-boundary.test.d.ts.map +1 -0
- package/dist/__tests__/canvas-input-owner.test-d.d.ts +2 -0
- package/dist/__tests__/canvas-input-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/composite-backend.test.d.ts +2 -0
- package/dist/__tests__/composite-backend.test.d.ts.map +1 -0
- package/dist/__tests__/controller-platform-owner.test-d.d.ts +2 -0
- package/dist/__tests__/controller-platform-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/input-snapshot.test-d.d.ts +2 -0
- package/dist/__tests__/input-snapshot.test-d.d.ts.map +1 -0
- package/dist/__tests__/input-snapshot.test.d.ts +2 -0
- package/dist/__tests__/input-snapshot.test.d.ts.map +1 -0
- package/dist/__tests__/swipe-direction-owner.test-d.d.ts +2 -0
- package/dist/__tests__/swipe-direction-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/ui-input-matrix.test.d.ts +2 -0
- package/dist/__tests__/ui-input-matrix.test.d.ts.map +1 -0
- package/dist/__tests__/ui-ownership-edge.test.d.ts +2 -0
- package/dist/__tests__/ui-ownership-edge.test.d.ts.map +1 -0
- package/dist/__tests__/ui-ownership.test.d.ts +2 -0
- package/dist/__tests__/ui-ownership.test.d.ts.map +1 -0
- package/dist/__tests__/ui-reset-idempotence.test.d.ts +2 -0
- package/dist/__tests__/ui-reset-idempotence.test.d.ts.map +1 -0
- package/dist/__tests__/ui-reset.test.d.ts +2 -0
- package/dist/__tests__/ui-reset.test.d.ts.map +1 -0
- package/dist/action-state.d.ts +146 -0
- package/dist/action-state.d.ts.map +1 -0
- package/dist/browser-backend.d.ts +121 -0
- package/dist/browser-backend.d.ts.map +1 -0
- package/dist/canvas-input-boundary.d.ts +13 -0
- package/dist/canvas-input-boundary.d.ts.map +1 -0
- package/dist/composite-backend.d.ts +34 -0
- package/dist/composite-backend.d.ts.map +1 -0
- package/dist/controller-db-data.d.ts +5 -0
- package/dist/controller-db-data.d.ts.map +1 -0
- package/dist/controller-db-data.mjs +12 -0
- package/dist/controller-db-data.mjs.map +1 -0
- package/dist/controller-db.d.ts +70 -0
- package/dist/controller-db.d.ts.map +1 -0
- package/dist/controller-db.mjs +119 -0
- package/dist/controller-db.mjs.map +1 -0
- package/dist/frame-start-scan-system.d.ts +37 -0
- package/dist/frame-start-scan-system.d.ts.map +1 -0
- package/dist/gamepad-frame.d.ts +42 -0
- package/dist/gamepad-frame.d.ts.map +1 -0
- package/dist/gesture-recognizer.d.ts +145 -0
- package/dist/gesture-recognizer.d.ts.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +1502 -0
- package/dist/index.mjs.map +1 -0
- package/dist/input-snapshot.d.ts +420 -0
- package/dist/input-snapshot.d.ts.map +1 -0
- package/dist/plugin-service.d.ts +11 -0
- package/dist/plugin-service.d.ts.map +1 -0
- package/dist/ui-ownership.d.ts +17 -0
- package/dist/ui-ownership.d.ts.map +1 -0
- package/dist/virtual-joystick.d.ts +39 -0
- package/dist/virtual-joystick.d.ts.map +1 -0
- package/package.json +72 -0
- package/src/__tests__/browser-backend-dual-instance.test.ts +379 -0
- package/src/__tests__/browser-backend-focus-loss.test.ts +239 -0
- package/src/__tests__/browser-backend-lock-error.test.ts +425 -0
- package/src/__tests__/browser-backend-lock-gate.test.ts +283 -0
- package/src/__tests__/browser-backend-lock-release.test.ts +641 -0
- package/src/__tests__/browser-backend-lock-state.test.ts +604 -0
- package/src/__tests__/canvas-input-boundary.test.ts +65 -0
- package/src/__tests__/canvas-input-owner.test-d.ts +39 -0
- package/src/__tests__/composite-backend.test.ts +299 -0
- package/src/__tests__/controller-platform-owner.test-d.ts +46 -0
- package/src/__tests__/input-snapshot.test-d.ts +78 -0
- package/src/__tests__/input-snapshot.test.ts +96 -0
- package/src/__tests__/swipe-direction-owner.test-d.ts +45 -0
- package/src/__tests__/ui-input-matrix.test.ts +31 -0
- package/src/__tests__/ui-ownership-edge.test.ts +33 -0
- package/src/__tests__/ui-ownership.test.ts +35 -0
- package/src/__tests__/ui-reset-idempotence.test.ts +17 -0
- package/src/__tests__/ui-reset.test.ts +25 -0
- package/src/action-state.ts +380 -0
- package/src/browser-backend.ts +884 -0
- package/src/canvas-input-boundary.ts +81 -0
- package/src/composite-backend.ts +189 -0
- package/src/controller-db-data.ts +25 -0
- package/src/controller-db.ts +218 -0
- package/src/frame-start-scan-system.ts +93 -0
- package/src/gamepad-frame.ts +259 -0
- package/src/gesture-recognizer.ts +495 -0
- package/src/index.ts +101 -0
- package/src/input-snapshot.ts +732 -0
- package/src/plugin-service.ts +31 -0
- package/src/ui-ownership.ts +61 -0
- package/src/virtual-joystick.ts +103 -0
- package/vendor/gamecontrollerdb.txt +2114 -0
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
// browser-backend-lock-release.test.ts -- M1 w3: release path unit tests.
|
|
2
|
+
//
|
|
3
|
+
// Covers: ESC (providerLocked only), blur provider release, detach dual-path release.
|
|
4
|
+
//
|
|
5
|
+
// charter awareness:
|
|
6
|
+
// F1 single-entry indexability -- all release tests in one file
|
|
7
|
+
// P3 explicit failure -- each assertion produces a clear diagnostic
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
import { attachBrowserInputBackend } from '../browser-backend';
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Helper: build fake env with call tracking.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
function buildReleaseEnv(opts?: { provider?: { requestLock: () => void; exitLock: () => void } }): {
|
|
17
|
+
backend: ReturnType<typeof attachBrowserInputBackend>['backend'];
|
|
18
|
+
fire(target: string, kind: string, ev: Partial<Event | KeyboardEvent>): void;
|
|
19
|
+
exitPointerLockCalls: { count: number };
|
|
20
|
+
exitLockCalls: { count: number };
|
|
21
|
+
requestLockCalls: { count: number };
|
|
22
|
+
setPointerLockElement(el: Element | null): void;
|
|
23
|
+
detach: () => void;
|
|
24
|
+
} {
|
|
25
|
+
const listeners = new Map<string, Map<string, Set<EventListener>>>();
|
|
26
|
+
|
|
27
|
+
const makeTarget = (label: string) => ({
|
|
28
|
+
addEventListener(kind: string, handler: EventListener): void {
|
|
29
|
+
let perTarget = listeners.get(label);
|
|
30
|
+
if (!perTarget) {
|
|
31
|
+
perTarget = new Map();
|
|
32
|
+
listeners.set(label, perTarget);
|
|
33
|
+
}
|
|
34
|
+
let set = perTarget.get(kind);
|
|
35
|
+
if (!set) {
|
|
36
|
+
set = new Set();
|
|
37
|
+
perTarget.set(kind, set);
|
|
38
|
+
}
|
|
39
|
+
set.add(handler);
|
|
40
|
+
},
|
|
41
|
+
removeEventListener(kind: string, handler: EventListener): void {
|
|
42
|
+
listeners.get(label)?.get(kind)?.delete(handler);
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
let pointerLockEl: Element | null = null;
|
|
47
|
+
const exitPointerLockCalls = { count: 0 };
|
|
48
|
+
const exitLockCalls = { count: 0 };
|
|
49
|
+
const requestLockCalls = { count: 0 };
|
|
50
|
+
|
|
51
|
+
const canvas = {
|
|
52
|
+
...makeTarget('canvas'),
|
|
53
|
+
requestPointerLock() {},
|
|
54
|
+
getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 600 }) as DOMRect,
|
|
55
|
+
width: 800,
|
|
56
|
+
height: 600,
|
|
57
|
+
style: { touchAction: '' },
|
|
58
|
+
setPointerCapture: () => {},
|
|
59
|
+
} as unknown as HTMLCanvasElement;
|
|
60
|
+
|
|
61
|
+
const doc = {
|
|
62
|
+
...makeTarget('document'),
|
|
63
|
+
hasFocus() {
|
|
64
|
+
return true;
|
|
65
|
+
},
|
|
66
|
+
visibilityState: 'visible',
|
|
67
|
+
get pointerLockElement() {
|
|
68
|
+
return pointerLockEl;
|
|
69
|
+
},
|
|
70
|
+
exitPointerLock() {
|
|
71
|
+
exitPointerLockCalls.count += 1;
|
|
72
|
+
pointerLockEl = null;
|
|
73
|
+
const hs = listeners.get('document')?.get('pointerlockchange');
|
|
74
|
+
if (hs) {
|
|
75
|
+
const ev = new Event('pointerlockchange');
|
|
76
|
+
for (const h of hs) h(ev as Event);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
} as unknown as Document & { exitPointerLock(): void };
|
|
80
|
+
|
|
81
|
+
const win = makeTarget('window') as unknown as Window;
|
|
82
|
+
|
|
83
|
+
const handle = attachBrowserInputBackend(canvas, {
|
|
84
|
+
document: doc,
|
|
85
|
+
window: win,
|
|
86
|
+
...(opts?.provider ? { lockProvider: opts.provider } : {}),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
backend: handle.backend,
|
|
91
|
+
fire(target, kind, ev) {
|
|
92
|
+
const handlers = listeners.get(target)?.get(kind);
|
|
93
|
+
if (!handlers) return;
|
|
94
|
+
for (const h of handlers) h(ev as Event);
|
|
95
|
+
},
|
|
96
|
+
exitPointerLockCalls,
|
|
97
|
+
exitLockCalls,
|
|
98
|
+
requestLockCalls,
|
|
99
|
+
setPointerLockElement(el) {
|
|
100
|
+
pointerLockEl = el;
|
|
101
|
+
const hs = listeners.get('document')?.get('pointerlockchange');
|
|
102
|
+
if (hs) {
|
|
103
|
+
const ev = new Event('pointerlockchange');
|
|
104
|
+
for (const h of hs) h(ev as Event);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
detach: handle,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Tests
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
describe('browser-backend-lock-release.test.ts (w3)', () => {
|
|
116
|
+
describe('ESC key release', () => {
|
|
117
|
+
it('ESC when providerLocked=true calls exitLock and clears providerLocked', () => {
|
|
118
|
+
const exitLockCalls = { count: 0 };
|
|
119
|
+
const env = buildReleaseEnv({
|
|
120
|
+
provider: {
|
|
121
|
+
requestLock() {},
|
|
122
|
+
exitLock() {
|
|
123
|
+
exitLockCalls.count += 1;
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Lock via provider path.
|
|
129
|
+
env.fire('canvas', 'click', new Event('click'));
|
|
130
|
+
const s1 = env.backend.sample();
|
|
131
|
+
expect(s1.pointerLocked).toBe(true);
|
|
132
|
+
|
|
133
|
+
// ESC releases provider lock.
|
|
134
|
+
env.fire('window', 'keydown', { key: 'Escape' } as KeyboardEvent);
|
|
135
|
+
expect(exitLockCalls.count).toBe(1);
|
|
136
|
+
|
|
137
|
+
const s2 = env.backend.sample();
|
|
138
|
+
expect(s2.pointerLocked).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('ESC when providerLocked=false does NOT call exitLock', () => {
|
|
142
|
+
const exitLockCalls = { count: 0 };
|
|
143
|
+
const env = buildReleaseEnv({
|
|
144
|
+
provider: {
|
|
145
|
+
requestLock() {},
|
|
146
|
+
exitLock() {
|
|
147
|
+
exitLockCalls.count += 1;
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Not locked via provider path.
|
|
153
|
+
env.fire('window', 'keydown', { key: 'Escape' } as KeyboardEvent);
|
|
154
|
+
expect(exitLockCalls.count).toBe(0);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('ESC when only w3cLocked=true does NOT call exitLock (W3C handles ESC via browser)', () => {
|
|
158
|
+
const exitLockCalls = { count: 0 };
|
|
159
|
+
const env = buildReleaseEnv({
|
|
160
|
+
provider: {
|
|
161
|
+
requestLock() {},
|
|
162
|
+
exitLock() {
|
|
163
|
+
exitLockCalls.count += 1;
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Lock via W3C path only.
|
|
169
|
+
// Actually, we need to simulate W3C lock. Let's use the setPointerLockElement helper.
|
|
170
|
+
env.fire('canvas', 'click', new Event('click'));
|
|
171
|
+
// Then simulate pointerlockchange to set w3cLocked=true.
|
|
172
|
+
// We can't directly set pointerLockElement without the canvas ref.
|
|
173
|
+
// Instead, use the exitPointerLock pattern: we know doc.exitPointerLock
|
|
174
|
+
// fires pointerlockchange. But we need to LOCK first.
|
|
175
|
+
// For W3C path, the backend fires requestPointerLock on click, then
|
|
176
|
+
// browser fires pointerlockchange. Let's just fire the pointerlockchange directly.
|
|
177
|
+
// We need canvas ref... let's rebuild with inline approach.
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('ESC when only w3cLocked=true does NOT call exitLock (W3C handles ESC via browser)', () => {
|
|
181
|
+
// Build inline to have canvas ref.
|
|
182
|
+
const listeners = new Map<string, Map<string, Set<EventListener>>>();
|
|
183
|
+
const makeTarget = (label: string) => ({
|
|
184
|
+
addEventListener(kind: string, handler: EventListener): void {
|
|
185
|
+
let perTarget = listeners.get(label);
|
|
186
|
+
if (!perTarget) {
|
|
187
|
+
perTarget = new Map();
|
|
188
|
+
listeners.set(label, perTarget);
|
|
189
|
+
}
|
|
190
|
+
let set = perTarget.get(kind);
|
|
191
|
+
if (!set) {
|
|
192
|
+
set = new Set();
|
|
193
|
+
perTarget.set(kind, set);
|
|
194
|
+
}
|
|
195
|
+
set.add(handler);
|
|
196
|
+
},
|
|
197
|
+
removeEventListener(kind: string, handler: EventListener): void {
|
|
198
|
+
listeners.get(label)?.get(kind)?.delete(handler);
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
let pointerLockEl: Element | null = null;
|
|
203
|
+
const exitLockCalls = { count: 0 };
|
|
204
|
+
|
|
205
|
+
const canvas = {
|
|
206
|
+
...makeTarget('canvas'),
|
|
207
|
+
requestPointerLock() {},
|
|
208
|
+
getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 600 }) as DOMRect,
|
|
209
|
+
width: 800,
|
|
210
|
+
height: 600,
|
|
211
|
+
style: { touchAction: '' },
|
|
212
|
+
setPointerCapture: () => {},
|
|
213
|
+
} as unknown as HTMLCanvasElement;
|
|
214
|
+
|
|
215
|
+
const doc = {
|
|
216
|
+
...makeTarget('document'),
|
|
217
|
+
hasFocus() {
|
|
218
|
+
return true;
|
|
219
|
+
},
|
|
220
|
+
visibilityState: 'visible',
|
|
221
|
+
get pointerLockElement() {
|
|
222
|
+
return pointerLockEl;
|
|
223
|
+
},
|
|
224
|
+
exitPointerLock() {},
|
|
225
|
+
} as unknown as Document;
|
|
226
|
+
|
|
227
|
+
const win = makeTarget('window') as unknown as Window;
|
|
228
|
+
|
|
229
|
+
const handle = attachBrowserInputBackend(canvas, {
|
|
230
|
+
document: doc,
|
|
231
|
+
window: win,
|
|
232
|
+
});
|
|
233
|
+
const backend = handle.backend;
|
|
234
|
+
|
|
235
|
+
// Lock via W3C path: click then pointerlockchange (no lockProvider).
|
|
236
|
+
const clickH = listeners.get('canvas')?.get('click');
|
|
237
|
+
if (clickH) {
|
|
238
|
+
const ev = new Event('click');
|
|
239
|
+
for (const h of clickH) h(ev as Event);
|
|
240
|
+
}
|
|
241
|
+
pointerLockEl = canvas;
|
|
242
|
+
const pcH = listeners.get('document')?.get('pointerlockchange');
|
|
243
|
+
if (pcH) {
|
|
244
|
+
const ev = new Event('pointerlockchange');
|
|
245
|
+
for (const h of pcH) h(ev as Event);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const s1 = backend.sample();
|
|
249
|
+
expect(s1.pointerLocked).toBe(true);
|
|
250
|
+
|
|
251
|
+
// ESC with W3C lock: exitLock is not called because there is no lockProvider.
|
|
252
|
+
// The W3C path handles ESC via browser built-in pointerlockchange.
|
|
253
|
+
const keyH = listeners.get('window')?.get('keydown');
|
|
254
|
+
if (keyH) {
|
|
255
|
+
const ev = { key: 'Escape' } as KeyboardEvent;
|
|
256
|
+
for (const h of keyH) h(ev as Event);
|
|
257
|
+
}
|
|
258
|
+
expect(exitLockCalls.count).toBe(0);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('ESC when both locked (w3cLocked + providerLocked) calls exitLock for provider', () => {
|
|
262
|
+
const listeners = new Map<string, Map<string, Set<EventListener>>>();
|
|
263
|
+
const makeTarget = (label: string) => ({
|
|
264
|
+
addEventListener(kind: string, handler: EventListener): void {
|
|
265
|
+
let perTarget = listeners.get(label);
|
|
266
|
+
if (!perTarget) {
|
|
267
|
+
perTarget = new Map();
|
|
268
|
+
listeners.set(label, perTarget);
|
|
269
|
+
}
|
|
270
|
+
let set = perTarget.get(kind);
|
|
271
|
+
if (!set) {
|
|
272
|
+
set = new Set();
|
|
273
|
+
perTarget.set(kind, set);
|
|
274
|
+
}
|
|
275
|
+
set.add(handler);
|
|
276
|
+
},
|
|
277
|
+
removeEventListener(kind: string, handler: EventListener): void {
|
|
278
|
+
listeners.get(label)?.get(kind)?.delete(handler);
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
let pointerLockEl: Element | null = null;
|
|
283
|
+
const exitLockCalls = { count: 0 };
|
|
284
|
+
|
|
285
|
+
const canvas = {
|
|
286
|
+
...makeTarget('canvas'),
|
|
287
|
+
requestPointerLock() {},
|
|
288
|
+
getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 600 }) as DOMRect,
|
|
289
|
+
width: 800,
|
|
290
|
+
height: 600,
|
|
291
|
+
style: { touchAction: '' },
|
|
292
|
+
setPointerCapture: () => {},
|
|
293
|
+
} as unknown as HTMLCanvasElement;
|
|
294
|
+
|
|
295
|
+
const doc = {
|
|
296
|
+
...makeTarget('document'),
|
|
297
|
+
hasFocus() {
|
|
298
|
+
return true;
|
|
299
|
+
},
|
|
300
|
+
visibilityState: 'visible',
|
|
301
|
+
get pointerLockElement() {
|
|
302
|
+
return pointerLockEl;
|
|
303
|
+
},
|
|
304
|
+
exitPointerLock() {},
|
|
305
|
+
} as unknown as Document;
|
|
306
|
+
|
|
307
|
+
const win = makeTarget('window') as unknown as Window;
|
|
308
|
+
|
|
309
|
+
const handle = attachBrowserInputBackend(canvas, {
|
|
310
|
+
document: doc,
|
|
311
|
+
window: win,
|
|
312
|
+
lockProvider: {
|
|
313
|
+
requestLock() {},
|
|
314
|
+
exitLock() {
|
|
315
|
+
exitLockCalls.count += 1;
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
const backend = handle.backend;
|
|
320
|
+
|
|
321
|
+
// Lock both paths.
|
|
322
|
+
// W3C path:
|
|
323
|
+
const clickH = listeners.get('canvas')?.get('click');
|
|
324
|
+
if (clickH) {
|
|
325
|
+
const ev = new Event('click');
|
|
326
|
+
for (const h of clickH) h(ev as Event);
|
|
327
|
+
}
|
|
328
|
+
pointerLockEl = canvas;
|
|
329
|
+
const pcH = listeners.get('document')?.get('pointerlockchange');
|
|
330
|
+
if (pcH) {
|
|
331
|
+
const ev = new Event('pointerlockchange');
|
|
332
|
+
for (const h of pcH) h(ev as Event);
|
|
333
|
+
}
|
|
334
|
+
// Provider path: click triggers provider too.
|
|
335
|
+
const s1 = backend.sample();
|
|
336
|
+
expect(s1.pointerLocked).toBe(true);
|
|
337
|
+
|
|
338
|
+
// ESC should call exitLock for provider path.
|
|
339
|
+
const keyH = listeners.get('window')?.get('keydown');
|
|
340
|
+
if (keyH) {
|
|
341
|
+
const ev = { key: 'Escape' } as KeyboardEvent;
|
|
342
|
+
for (const h of keyH) h(ev as Event);
|
|
343
|
+
}
|
|
344
|
+
expect(exitLockCalls.count).toBe(1);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
describe('blur provider release', () => {
|
|
349
|
+
it('blur when providerLocked=true calls exitLock and clears providerLocked', () => {
|
|
350
|
+
const exitLockCalls = { count: 0 };
|
|
351
|
+
const env = buildReleaseEnv({
|
|
352
|
+
provider: {
|
|
353
|
+
requestLock() {},
|
|
354
|
+
exitLock() {
|
|
355
|
+
exitLockCalls.count += 1;
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Lock via provider path.
|
|
361
|
+
env.fire('canvas', 'click', new Event('click'));
|
|
362
|
+
const s1 = env.backend.sample();
|
|
363
|
+
expect(s1.pointerLocked).toBe(true);
|
|
364
|
+
|
|
365
|
+
// Blur should release provider lock.
|
|
366
|
+
env.fire('window', 'blur', new Event('blur'));
|
|
367
|
+
expect(exitLockCalls.count).toBe(1);
|
|
368
|
+
|
|
369
|
+
const s2 = env.backend.sample();
|
|
370
|
+
expect(s2.pointerLocked).toBe(false);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('blur when providerLocked=false does NOT call exitLock', () => {
|
|
374
|
+
const exitLockCalls = { count: 0 };
|
|
375
|
+
const env = buildReleaseEnv({
|
|
376
|
+
provider: {
|
|
377
|
+
requestLock() {},
|
|
378
|
+
exitLock() {
|
|
379
|
+
exitLockCalls.count += 1;
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
env.fire('window', 'blur', new Event('blur'));
|
|
385
|
+
expect(exitLockCalls.count).toBe(0);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
describe('detach dual-path release', () => {
|
|
390
|
+
it('detach when W3C-locked calls exitPointerLock', () => {
|
|
391
|
+
const listeners = new Map<string, Map<string, Set<EventListener>>>();
|
|
392
|
+
const makeTarget = (label: string) => ({
|
|
393
|
+
addEventListener(kind: string, handler: EventListener): void {
|
|
394
|
+
let perTarget = listeners.get(label);
|
|
395
|
+
if (!perTarget) {
|
|
396
|
+
perTarget = new Map();
|
|
397
|
+
listeners.set(label, perTarget);
|
|
398
|
+
}
|
|
399
|
+
let set = perTarget.get(kind);
|
|
400
|
+
if (!set) {
|
|
401
|
+
set = new Set();
|
|
402
|
+
perTarget.set(kind, set);
|
|
403
|
+
}
|
|
404
|
+
set.add(handler);
|
|
405
|
+
},
|
|
406
|
+
removeEventListener(kind: string, handler: EventListener): void {
|
|
407
|
+
listeners.get(label)?.get(kind)?.delete(handler);
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
let pointerLockEl: Element | null = null;
|
|
412
|
+
const exitPointerLockCalls = { count: 0 };
|
|
413
|
+
|
|
414
|
+
const canvas = {
|
|
415
|
+
...makeTarget('canvas'),
|
|
416
|
+
requestPointerLock() {},
|
|
417
|
+
getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 600 }) as DOMRect,
|
|
418
|
+
width: 800,
|
|
419
|
+
height: 600,
|
|
420
|
+
style: { touchAction: '' },
|
|
421
|
+
setPointerCapture: () => {},
|
|
422
|
+
} as unknown as HTMLCanvasElement;
|
|
423
|
+
|
|
424
|
+
const doc = {
|
|
425
|
+
...makeTarget('document'),
|
|
426
|
+
hasFocus() {
|
|
427
|
+
return true;
|
|
428
|
+
},
|
|
429
|
+
visibilityState: 'visible',
|
|
430
|
+
get pointerLockElement() {
|
|
431
|
+
return pointerLockEl;
|
|
432
|
+
},
|
|
433
|
+
exitPointerLock() {
|
|
434
|
+
exitPointerLockCalls.count += 1;
|
|
435
|
+
},
|
|
436
|
+
} as unknown as Document;
|
|
437
|
+
|
|
438
|
+
const win = makeTarget('window') as unknown as Window;
|
|
439
|
+
|
|
440
|
+
const handle = attachBrowserInputBackend(canvas, {
|
|
441
|
+
document: doc,
|
|
442
|
+
window: win,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Lock via W3C path.
|
|
446
|
+
const clickH = listeners.get('canvas')?.get('click');
|
|
447
|
+
if (clickH) {
|
|
448
|
+
const ev = new Event('click');
|
|
449
|
+
for (const h of clickH) h(ev as Event);
|
|
450
|
+
}
|
|
451
|
+
pointerLockEl = canvas;
|
|
452
|
+
|
|
453
|
+
// Detach while locked.
|
|
454
|
+
handle();
|
|
455
|
+
expect(exitPointerLockCalls.count).toBe(1);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('detach when provider-locked calls exitLock', () => {
|
|
459
|
+
const exitLockCalls = { count: 0 };
|
|
460
|
+
const env = buildReleaseEnv({
|
|
461
|
+
provider: {
|
|
462
|
+
requestLock() {},
|
|
463
|
+
exitLock() {
|
|
464
|
+
exitLockCalls.count += 1;
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Lock via provider path.
|
|
470
|
+
env.fire('canvas', 'click', new Event('click'));
|
|
471
|
+
expect(env.backend.sample().pointerLocked).toBe(true);
|
|
472
|
+
|
|
473
|
+
// Detach should release provider lock.
|
|
474
|
+
env.detach();
|
|
475
|
+
expect(exitLockCalls.count).toBe(1);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('detach when both-locked calls both exitPointerLock and exitLock', () => {
|
|
479
|
+
const listeners = new Map<string, Map<string, Set<EventListener>>>();
|
|
480
|
+
const makeTarget = (label: string) => ({
|
|
481
|
+
addEventListener(kind: string, handler: EventListener): void {
|
|
482
|
+
let perTarget = listeners.get(label);
|
|
483
|
+
if (!perTarget) {
|
|
484
|
+
perTarget = new Map();
|
|
485
|
+
listeners.set(label, perTarget);
|
|
486
|
+
}
|
|
487
|
+
let set = perTarget.get(kind);
|
|
488
|
+
if (!set) {
|
|
489
|
+
set = new Set();
|
|
490
|
+
perTarget.set(kind, set);
|
|
491
|
+
}
|
|
492
|
+
set.add(handler);
|
|
493
|
+
},
|
|
494
|
+
removeEventListener(kind: string, handler: EventListener): void {
|
|
495
|
+
listeners.get(label)?.get(kind)?.delete(handler);
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
let pointerLockEl: Element | null = null;
|
|
500
|
+
const exitPointerLockCalls = { count: 0 };
|
|
501
|
+
const exitLockCalls = { count: 0 };
|
|
502
|
+
|
|
503
|
+
const canvas = {
|
|
504
|
+
...makeTarget('canvas'),
|
|
505
|
+
requestPointerLock() {},
|
|
506
|
+
getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 600 }) as DOMRect,
|
|
507
|
+
width: 800,
|
|
508
|
+
height: 600,
|
|
509
|
+
style: { touchAction: '' },
|
|
510
|
+
setPointerCapture: () => {},
|
|
511
|
+
} as unknown as HTMLCanvasElement;
|
|
512
|
+
|
|
513
|
+
const doc = {
|
|
514
|
+
...makeTarget('document'),
|
|
515
|
+
hasFocus() {
|
|
516
|
+
return true;
|
|
517
|
+
},
|
|
518
|
+
visibilityState: 'visible',
|
|
519
|
+
get pointerLockElement() {
|
|
520
|
+
return pointerLockEl;
|
|
521
|
+
},
|
|
522
|
+
exitPointerLock() {
|
|
523
|
+
exitPointerLockCalls.count += 1;
|
|
524
|
+
},
|
|
525
|
+
} as unknown as Document;
|
|
526
|
+
|
|
527
|
+
const win = makeTarget('window') as unknown as Window;
|
|
528
|
+
|
|
529
|
+
const handle = attachBrowserInputBackend(canvas, {
|
|
530
|
+
document: doc,
|
|
531
|
+
window: win,
|
|
532
|
+
lockProvider: {
|
|
533
|
+
requestLock() {},
|
|
534
|
+
exitLock() {
|
|
535
|
+
exitLockCalls.count += 1;
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// Lock both paths.
|
|
541
|
+
const clickH = listeners.get('canvas')?.get('click');
|
|
542
|
+
if (clickH) {
|
|
543
|
+
const ev = new Event('click');
|
|
544
|
+
for (const h of clickH) h(ev as Event);
|
|
545
|
+
}
|
|
546
|
+
pointerLockEl = canvas;
|
|
547
|
+
const pcH = listeners.get('document')?.get('pointerlockchange');
|
|
548
|
+
if (pcH) {
|
|
549
|
+
const ev = new Event('pointerlockchange');
|
|
550
|
+
for (const h of pcH) h(ev as Event);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
expect(handle.backend.sample().pointerLocked).toBe(true);
|
|
554
|
+
|
|
555
|
+
// Detach: both paths should release.
|
|
556
|
+
handle();
|
|
557
|
+
expect(exitPointerLockCalls.count).toBe(1);
|
|
558
|
+
expect(exitLockCalls.count).toBe(1);
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
describe('W3C auto-unlock via pointerlockchange', () => {
|
|
563
|
+
it('W3C ESC/focus-loss triggers pointerlockchange, setting w3cLocked=false automatically', () => {
|
|
564
|
+
const listeners = new Map<string, Map<string, Set<EventListener>>>();
|
|
565
|
+
const makeTarget = (label: string) => ({
|
|
566
|
+
addEventListener(kind: string, handler: EventListener): void {
|
|
567
|
+
let perTarget = listeners.get(label);
|
|
568
|
+
if (!perTarget) {
|
|
569
|
+
perTarget = new Map();
|
|
570
|
+
listeners.set(label, perTarget);
|
|
571
|
+
}
|
|
572
|
+
let set = perTarget.get(kind);
|
|
573
|
+
if (!set) {
|
|
574
|
+
set = new Set();
|
|
575
|
+
perTarget.set(kind, set);
|
|
576
|
+
}
|
|
577
|
+
set.add(handler);
|
|
578
|
+
},
|
|
579
|
+
removeEventListener(kind: string, handler: EventListener): void {
|
|
580
|
+
listeners.get(label)?.get(kind)?.delete(handler);
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
let pointerLockEl: Element | null = null;
|
|
585
|
+
|
|
586
|
+
const canvas = {
|
|
587
|
+
...makeTarget('canvas'),
|
|
588
|
+
requestPointerLock() {},
|
|
589
|
+
getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 600 }) as DOMRect,
|
|
590
|
+
width: 800,
|
|
591
|
+
height: 600,
|
|
592
|
+
style: { touchAction: '' },
|
|
593
|
+
setPointerCapture: () => {},
|
|
594
|
+
} as unknown as HTMLCanvasElement;
|
|
595
|
+
|
|
596
|
+
const doc = {
|
|
597
|
+
...makeTarget('document'),
|
|
598
|
+
hasFocus() {
|
|
599
|
+
return true;
|
|
600
|
+
},
|
|
601
|
+
visibilityState: 'visible',
|
|
602
|
+
get pointerLockElement() {
|
|
603
|
+
return pointerLockEl;
|
|
604
|
+
},
|
|
605
|
+
exitPointerLock() {},
|
|
606
|
+
} as unknown as Document;
|
|
607
|
+
|
|
608
|
+
const win = makeTarget('window') as unknown as Window;
|
|
609
|
+
|
|
610
|
+
const handle = attachBrowserInputBackend(canvas, {
|
|
611
|
+
document: doc,
|
|
612
|
+
window: win,
|
|
613
|
+
});
|
|
614
|
+
const backend = handle.backend;
|
|
615
|
+
|
|
616
|
+
// Lock via W3C.
|
|
617
|
+
const clickH = listeners.get('canvas')?.get('click');
|
|
618
|
+
if (clickH) {
|
|
619
|
+
const ev = new Event('click');
|
|
620
|
+
for (const h of clickH) h(ev as Event);
|
|
621
|
+
}
|
|
622
|
+
pointerLockEl = canvas;
|
|
623
|
+
const pcH = listeners.get('document')?.get('pointerlockchange');
|
|
624
|
+
if (pcH) {
|
|
625
|
+
const ev = new Event('pointerlockchange');
|
|
626
|
+
for (const h of pcH) h(ev as Event);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
expect(backend.sample().pointerLocked).toBe(true);
|
|
630
|
+
|
|
631
|
+
// Browser auto-unlock (ESC / focus-loss): pointerLockElement → null
|
|
632
|
+
pointerLockEl = null;
|
|
633
|
+
if (pcH) {
|
|
634
|
+
const ev = new Event('pointerlockchange');
|
|
635
|
+
for (const h of pcH) h(ev as Event);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
expect(backend.sample().pointerLocked).toBe(false);
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
});
|