@forgeax/engine-input 0.1.20 → 0.1.21
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/README.md +15 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/browser-backend-edge-latch.test.d.ts +2 -0
- package/dist/__tests__/browser-backend-edge-latch.test.d.ts.map +1 -0
- package/dist/__tests__/browser-edge-latch.browser.test.d.ts +2 -0
- package/dist/__tests__/browser-edge-latch.browser.test.d.ts.map +1 -0
- package/dist/__tests__/pointer-lock.browser.test.d.ts +2 -0
- package/dist/__tests__/pointer-lock.browser.test.d.ts.map +1 -0
- package/dist/browser-backend.d.ts.map +1 -1
- package/dist/composite-backend.d.ts +4 -1
- package/dist/composite-backend.d.ts.map +1 -1
- package/dist/frame-start-scan-system.d.ts +2 -2
- package/dist/index.mjs +118 -35
- package/dist/index.mjs.map +1 -1
- package/dist/input-snapshot.d.ts +13 -3
- package/dist/input-snapshot.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/browser-backend-edge-latch.test.ts +154 -0
- package/src/__tests__/browser-backend-focus-loss.test.ts +4 -0
- package/src/__tests__/browser-backend-lock-error.test.ts +25 -11
- package/src/__tests__/browser-backend-lock-gate.test.ts +8 -1
- package/src/__tests__/browser-edge-latch.browser.test.ts +42 -0
- package/src/__tests__/composite-backend.test.ts +18 -0
- package/src/__tests__/input-snapshot.test.ts +35 -0
- package/src/__tests__/pointer-lock.browser.test.ts +59 -0
- package/src/browser-backend.ts +71 -19
- package/src/composite-backend.ts +70 -3
- package/src/frame-start-scan-system.ts +2 -2
- package/src/input-snapshot.ts +45 -25
package/dist/input-snapshot.d.ts
CHANGED
|
@@ -307,12 +307,14 @@ export interface InputSnapshot {
|
|
|
307
307
|
export interface InputBackend {
|
|
308
308
|
/**
|
|
309
309
|
* Produce one frame's worth of input data and reset the per-frame
|
|
310
|
-
* accumulators (movement delta +
|
|
311
|
-
* held-button state survive across calls.
|
|
310
|
+
* accumulators (movement delta + edge sets). Held-key state and
|
|
311
|
+
* held-button state survive across calls. Browser producers latch edge
|
|
312
|
+
* transitions when events arrive, so a down+up sequence between two scans
|
|
313
|
+
* remains observable instead of being lost to the held-state projection.
|
|
312
314
|
*
|
|
313
315
|
* Contract notes:
|
|
314
316
|
* - `downKeys` is the held-key set as of the call (held across frames)
|
|
315
|
-
* - `upKeys` is the
|
|
317
|
+
* - `upKeys` is the release-edge set since the previous sample (cleared
|
|
316
318
|
* after the call so the edge appears in exactly one frame)
|
|
317
319
|
* - `buttons` is the held-button slot tuple `[b0, b1, b2]`
|
|
318
320
|
* - `movementX` / `movementY` are the accumulated PointerLock delta
|
|
@@ -349,7 +351,15 @@ export interface InputBackendSample {
|
|
|
349
351
|
readonly downCodes?: ReadonlySet<string>;
|
|
350
352
|
/** One-frame physical code release edges, when the producer has them. */
|
|
351
353
|
readonly upCodes?: ReadonlySet<string>;
|
|
354
|
+
/** Logical key press edges latched since the previous frame-start scan. */
|
|
355
|
+
readonly pressedKeys?: ReadonlySet<string>;
|
|
356
|
+
/** Physical code press edges latched since the previous frame-start scan. */
|
|
357
|
+
readonly pressedCodes?: ReadonlySet<string>;
|
|
352
358
|
readonly buttons: readonly [boolean, boolean, boolean];
|
|
359
|
+
/** Mouse button press edges latched since the previous frame-start scan. */
|
|
360
|
+
readonly pressedButtons?: readonly [boolean, boolean, boolean];
|
|
361
|
+
/** Mouse button release edges latched since the previous frame-start scan. */
|
|
362
|
+
readonly releasedButtons?: readonly [boolean, boolean, boolean];
|
|
353
363
|
readonly movementX: number;
|
|
354
364
|
readonly movementY: number;
|
|
355
365
|
/** Latest canvas-pixel mouse position; absent for non-pointer backends. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input-snapshot.d.ts","sourceRoot":"","sources":["../src/input-snapshot.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"input-snapshot.d.ts","sourceRoot":"","sources":["../src/input-snapshot.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgeax/engine-input",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"LICENSE"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@forgeax/engine-ecs": "0.1.
|
|
35
|
-
"@forgeax/engine-plugin": "0.1.
|
|
34
|
+
"@forgeax/engine-ecs": "0.1.21",
|
|
35
|
+
"@forgeax/engine-plugin": "0.1.21"
|
|
36
36
|
},
|
|
37
37
|
"forgeax": {
|
|
38
38
|
"metrics": {
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Update, World } from '@forgeax/engine-ecs';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { attachBrowserInputBackend } from '../browser-backend';
|
|
4
|
+
import { INPUT_BACKEND_KEY, InputFrameStartScan } from '../frame-start-scan-system';
|
|
5
|
+
import { INPUT_SNAPSHOT_RESOURCE_KEY, snapshotFromSample } from '../input-snapshot';
|
|
6
|
+
|
|
7
|
+
type Listener = (event: Event) => void;
|
|
8
|
+
|
|
9
|
+
function buildEnvironment(): {
|
|
10
|
+
readonly backend: ReturnType<typeof attachBrowserInputBackend>['backend'];
|
|
11
|
+
fire(target: 'canvas' | 'document' | 'window', kind: string, event: Event): void;
|
|
12
|
+
} {
|
|
13
|
+
const listeners = new Map<string, Map<string, Set<Listener>>>();
|
|
14
|
+
const target = (name: string) => ({
|
|
15
|
+
addEventListener(kind: string, listener: Listener): void {
|
|
16
|
+
let byKind = listeners.get(name);
|
|
17
|
+
if (byKind === undefined) {
|
|
18
|
+
byKind = new Map();
|
|
19
|
+
listeners.set(name, byKind);
|
|
20
|
+
}
|
|
21
|
+
let handlers = byKind.get(kind);
|
|
22
|
+
if (handlers === undefined) {
|
|
23
|
+
handlers = new Set();
|
|
24
|
+
byKind.set(kind, handlers);
|
|
25
|
+
}
|
|
26
|
+
handlers.add(listener);
|
|
27
|
+
},
|
|
28
|
+
removeEventListener(kind: string, listener: Listener): void {
|
|
29
|
+
listeners.get(name)?.get(kind)?.delete(listener);
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
const canvas = {
|
|
33
|
+
...target('canvas'),
|
|
34
|
+
width: 800,
|
|
35
|
+
height: 600,
|
|
36
|
+
style: { touchAction: '' },
|
|
37
|
+
getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 600 }) as DOMRect,
|
|
38
|
+
requestPointerLock: () => {},
|
|
39
|
+
setPointerCapture: () => {},
|
|
40
|
+
} as unknown as HTMLCanvasElement;
|
|
41
|
+
const document = {
|
|
42
|
+
...target('document'),
|
|
43
|
+
hasFocus: () => true,
|
|
44
|
+
visibilityState: 'visible',
|
|
45
|
+
pointerLockElement: null,
|
|
46
|
+
exitPointerLock: () => {},
|
|
47
|
+
} as unknown as Document;
|
|
48
|
+
const window = target('window') as unknown as Window;
|
|
49
|
+
const handle = attachBrowserInputBackend(canvas, { document, window });
|
|
50
|
+
return {
|
|
51
|
+
backend: handle.backend,
|
|
52
|
+
fire(targetName, kind, event) {
|
|
53
|
+
for (const listener of listeners.get(targetName)?.get(kind) ?? []) listener(event);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function keyEvent(key: string, code: string, repeat = false): Event {
|
|
59
|
+
return { key, code, repeat } as unknown as Event;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function pointerEvent(type: 'pointerdown' | 'pointerup', button: 0 | 1 | 2): Event {
|
|
63
|
+
return {
|
|
64
|
+
pointerId: 1,
|
|
65
|
+
pointerType: 'mouse',
|
|
66
|
+
button,
|
|
67
|
+
clientX: 100,
|
|
68
|
+
clientY: 100,
|
|
69
|
+
pressure: type === 'pointerdown' ? 0.5 : 0,
|
|
70
|
+
movementX: 0,
|
|
71
|
+
movementY: 0,
|
|
72
|
+
} as unknown as Event;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe('browser input edge latch', () => {
|
|
76
|
+
it('preserves key and mouse down/up transitions that arrive between scans', () => {
|
|
77
|
+
const env = buildEnvironment();
|
|
78
|
+
env.fire('window', 'keydown', keyEvent('a', 'KeyA'));
|
|
79
|
+
env.fire('window', 'keyup', keyEvent('a', 'KeyA'));
|
|
80
|
+
env.fire('canvas', 'pointerdown', pointerEvent('pointerdown', 0));
|
|
81
|
+
env.fire('canvas', 'pointerup', pointerEvent('pointerup', 0));
|
|
82
|
+
|
|
83
|
+
const sample = env.backend.sample();
|
|
84
|
+
expect(sample.downKeys.has('a')).toBe(false);
|
|
85
|
+
expect(sample.upKeys.has('a')).toBe(true);
|
|
86
|
+
expect(sample.pressedKeys?.has('a')).toBe(true);
|
|
87
|
+
expect(sample.downCodes?.has('KeyA')).toBe(false);
|
|
88
|
+
expect(sample.upCodes?.has('KeyA')).toBe(true);
|
|
89
|
+
expect(sample.pressedCodes?.has('KeyA')).toBe(true);
|
|
90
|
+
expect(sample.buttons).toEqual([false, false, false]);
|
|
91
|
+
expect(sample.pressedButtons).toEqual([true, false, false]);
|
|
92
|
+
expect(sample.releasedButtons).toEqual([true, false, false]);
|
|
93
|
+
|
|
94
|
+
const next = env.backend.sample();
|
|
95
|
+
expect(next.upKeys.size).toBe(0);
|
|
96
|
+
expect(next.pressedKeys?.size).toBe(0);
|
|
97
|
+
expect(next.upCodes?.size).toBe(0);
|
|
98
|
+
expect(next.pressedCodes?.size).toBe(0);
|
|
99
|
+
expect(next.pressedButtons).toEqual([false, false, false]);
|
|
100
|
+
expect(next.releasedButtons).toEqual([false, false, false]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('does not synthesize a second press for repeated keydown events', () => {
|
|
104
|
+
const env = buildEnvironment();
|
|
105
|
+
env.fire('window', 'keydown', keyEvent('w', 'KeyW'));
|
|
106
|
+
expect(env.backend.sample().pressedKeys?.has('w')).toBe(true);
|
|
107
|
+
|
|
108
|
+
env.fire('window', 'keydown', keyEvent('w', 'KeyW', true));
|
|
109
|
+
const repeated = env.backend.sample();
|
|
110
|
+
expect(repeated.downKeys.has('w')).toBe(true);
|
|
111
|
+
expect(repeated.pressedKeys?.has('w')).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('lets snapshot and frame-start scan consume the latched edges exactly once', () => {
|
|
115
|
+
const sample = {
|
|
116
|
+
downKeys: new Set<string>(),
|
|
117
|
+
upKeys: new Set(['a']),
|
|
118
|
+
downCodes: new Set<string>(),
|
|
119
|
+
upCodes: new Set(['KeyA']),
|
|
120
|
+
pressedKeys: new Set(['a']),
|
|
121
|
+
pressedCodes: new Set(['KeyA']),
|
|
122
|
+
buttons: [false, false, false] as const,
|
|
123
|
+
pressedButtons: [true, false, false] as const,
|
|
124
|
+
releasedButtons: [true, false, false] as const,
|
|
125
|
+
movementX: 0,
|
|
126
|
+
movementY: 0,
|
|
127
|
+
wheelDelta: 0,
|
|
128
|
+
focused: true,
|
|
129
|
+
pointerLocked: false,
|
|
130
|
+
};
|
|
131
|
+
const direct = snapshotFromSample(sample);
|
|
132
|
+
expect(direct.keyboard.justPressed('a')).toBe(true);
|
|
133
|
+
expect(direct.keyboard.justPressedCode('KeyA')).toBe(true);
|
|
134
|
+
expect(direct.mouse.justPressed(0)).toBe(true);
|
|
135
|
+
expect(direct.mouse.justReleased(0)).toBe(true);
|
|
136
|
+
expect(direct.mouse.button(0)).toBe(false);
|
|
137
|
+
|
|
138
|
+
let calls = 0;
|
|
139
|
+
const world = new World();
|
|
140
|
+
world.insertResource(INPUT_BACKEND_KEY, {
|
|
141
|
+
sample: () => {
|
|
142
|
+
calls += 1;
|
|
143
|
+
return sample;
|
|
144
|
+
},
|
|
145
|
+
detach: () => {},
|
|
146
|
+
});
|
|
147
|
+
world.addSystem(Update, InputFrameStartScan).unwrap();
|
|
148
|
+
world.update(0).unwrap();
|
|
149
|
+
const scanned = world.getResource<typeof direct>(INPUT_SNAPSHOT_RESOURCE_KEY);
|
|
150
|
+
expect(calls).toBe(1);
|
|
151
|
+
expect(scanned.keyboard.justPressed('a')).toBe(true);
|
|
152
|
+
expect(scanned.mouse.justReleased(0)).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -179,7 +179,11 @@ describe('browser-backend focus loss', () => {
|
|
|
179
179
|
const cancelled = env.backend.sample();
|
|
180
180
|
expect(cancelled.downKeys.size).toBe(0);
|
|
181
181
|
expect(cancelled.downCodes?.size).toBe(0);
|
|
182
|
+
expect(cancelled.pressedKeys?.size).toBe(0);
|
|
183
|
+
expect(cancelled.pressedCodes?.size).toBe(0);
|
|
182
184
|
expect(cancelled.buttons).toEqual([false, false, false]);
|
|
185
|
+
expect(cancelled.pressedButtons).toEqual([false, false, false]);
|
|
186
|
+
expect(cancelled.releasedButtons).toEqual([false, false, false]);
|
|
183
187
|
expect(cancelled.focusReset).toBe(true);
|
|
184
188
|
expect(cancelled.pointerLocked).toBe(false);
|
|
185
189
|
expect(cancelled.pointers).toBeUndefined();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// browser-backend-lock-error.test.ts --
|
|
1
|
+
// browser-backend-lock-error.test.ts -- M3 pointer-lock failure unit tests.
|
|
2
2
|
//
|
|
3
|
-
// Covers: W3C rejection → onLockError (no longer silently
|
|
4
|
-
// provider reject → onLockError + providerLocked rollback.
|
|
3
|
+
// Covers: W3C rejection / pointerlockerror → onLockError (no longer silently
|
|
4
|
+
// swallowed), provider reject → onLockError + providerLocked rollback.
|
|
5
5
|
//
|
|
6
6
|
// charter awareness:
|
|
7
7
|
// P3 explicit failure -- lock failures must produce structured signals,
|
|
@@ -84,14 +84,28 @@ describe('browser-backend-lock-error.test.ts (w4)', () => {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
// Wait for microtask to resolve promise rejection.
|
|
87
|
-
return vi
|
|
88
|
-
(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
87
|
+
return vi
|
|
88
|
+
.waitFor(
|
|
89
|
+
() => {
|
|
90
|
+
expect(lockErrorCalls.length).toBe(1);
|
|
91
|
+
expect(lockErrorCalls[0]?.path).toBe('w3c');
|
|
92
|
+
expect(lockErrorCalls[0]?.cause).toBe(rejectionError);
|
|
93
|
+
},
|
|
94
|
+
{ timeout: 200 },
|
|
95
|
+
)
|
|
96
|
+
.then(async () => {
|
|
97
|
+
const browserEvent = new Event('pointerlockerror');
|
|
98
|
+
const errorHandlers = listeners.get('document')?.get('pointerlockerror');
|
|
99
|
+
for (const handler of errorHandlers ?? []) handler(browserEvent);
|
|
100
|
+
await vi.waitFor(
|
|
101
|
+
() => {
|
|
102
|
+
expect(lockErrorCalls.length).toBe(2);
|
|
103
|
+
expect(lockErrorCalls[1]?.path).toBe('w3c');
|
|
104
|
+
expect(lockErrorCalls[1]?.cause).toBe(browserEvent);
|
|
105
|
+
},
|
|
106
|
+
{ timeout: 200 },
|
|
107
|
+
);
|
|
108
|
+
});
|
|
95
109
|
});
|
|
96
110
|
|
|
97
111
|
it('W3C rejection without onLockError provided does NOT throw (backend is resilient)', () => {
|
|
@@ -17,6 +17,7 @@ import { attachBrowserInputBackend } from '../browser-backend';
|
|
|
17
17
|
function buildEnvForClickTest(opts?: {
|
|
18
18
|
hostPredicate?: (() => boolean) | undefined;
|
|
19
19
|
lockProvider?: { requestLock: () => void; exitLock: () => void } | undefined;
|
|
20
|
+
focus?: boolean | undefined;
|
|
20
21
|
}): {
|
|
21
22
|
backend: ReturnType<typeof attachBrowserInputBackend>['backend'];
|
|
22
23
|
fireClick(): void;
|
|
@@ -68,7 +69,7 @@ function buildEnvForClickTest(opts?: {
|
|
|
68
69
|
const doc = {
|
|
69
70
|
...makeTarget('document'),
|
|
70
71
|
hasFocus() {
|
|
71
|
-
return true;
|
|
72
|
+
return opts?.focus ?? true;
|
|
72
73
|
},
|
|
73
74
|
visibilityState: 'visible',
|
|
74
75
|
get pointerLockElement() {
|
|
@@ -144,6 +145,12 @@ describe('browser-backend-lock-gate.test.ts (w2)', () => {
|
|
|
144
145
|
expect(env.backend.sample().pointerLocked).toBe(false); // pointerlockchange not fired yet
|
|
145
146
|
});
|
|
146
147
|
|
|
148
|
+
it('does not add a silent focus gate to a trusted click', () => {
|
|
149
|
+
const env = buildEnvForClickTest({ focus: false, hostPredicate: () => true });
|
|
150
|
+
env.fireClick();
|
|
151
|
+
expect(env.requestPointerLockCalls.count).toBe(1);
|
|
152
|
+
});
|
|
153
|
+
|
|
147
154
|
it('gameGate=true (default), hostPredicate=false -> no lock', () => {
|
|
148
155
|
const env = buildEnvForClickTest({
|
|
149
156
|
hostPredicate: () => false,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { userEvent } from 'vitest/browser';
|
|
3
|
+
import { attachBrowserInputBackend } from '../browser-backend';
|
|
4
|
+
|
|
5
|
+
describe('browser edge latch (real Chromium)', () => {
|
|
6
|
+
it('keeps trusted low-frequency down/up edges until the next sample', async () => {
|
|
7
|
+
const canvas = document.createElement('canvas');
|
|
8
|
+
canvas.width = 320;
|
|
9
|
+
canvas.height = 200;
|
|
10
|
+
canvas.tabIndex = 0;
|
|
11
|
+
document.body.append(canvas);
|
|
12
|
+
const handle = attachBrowserInputBackend(canvas, {
|
|
13
|
+
pointerLockAllowed: () => false,
|
|
14
|
+
});
|
|
15
|
+
try {
|
|
16
|
+
// This is a real Playwright user click. The gate is disabled so the
|
|
17
|
+
// fixture does not request a browser pointer lock as a side effect.
|
|
18
|
+
await userEvent.click(canvas);
|
|
19
|
+
handle.backend.sample();
|
|
20
|
+
|
|
21
|
+
// A single user-level key interaction produces both DOM events before
|
|
22
|
+
// the deliberately delayed frame-start sample.
|
|
23
|
+
await userEvent.keyboard('a');
|
|
24
|
+
const sample = handle.backend.sample();
|
|
25
|
+
expect(sample.downKeys.has('a')).toBe(false);
|
|
26
|
+
expect(sample.upKeys.has('a')).toBe(true);
|
|
27
|
+
expect(sample.pressedKeys?.has('a')).toBe(true);
|
|
28
|
+
expect(sample.downCodes?.has('KeyA')).toBe(false);
|
|
29
|
+
expect(sample.upCodes?.has('KeyA')).toBe(true);
|
|
30
|
+
expect(sample.pressedCodes?.has('KeyA')).toBe(true);
|
|
31
|
+
|
|
32
|
+
const next = handle.backend.sample();
|
|
33
|
+
expect(next.pressedKeys?.size).toBe(0);
|
|
34
|
+
expect(next.upKeys.size).toBe(0);
|
|
35
|
+
expect(next.pressedCodes?.size).toBe(0);
|
|
36
|
+
expect(next.upCodes?.size).toBe(0);
|
|
37
|
+
} finally {
|
|
38
|
+
handle();
|
|
39
|
+
canvas.remove();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -257,6 +257,24 @@ describe('CompositeInputBackend merge semantics', () => {
|
|
|
257
257
|
expect(s.upKeys.has('a')).toBe(true);
|
|
258
258
|
});
|
|
259
259
|
|
|
260
|
+
it('clearInjected drops pending press/release latches without ghost edges', () => {
|
|
261
|
+
const inner = fakeInner();
|
|
262
|
+
const c = makeCompositeBackend(inner);
|
|
263
|
+
c.press('w');
|
|
264
|
+
c.setButton(0, true);
|
|
265
|
+
c.release('w');
|
|
266
|
+
c.setButton(0, false);
|
|
267
|
+
c.clearInjected();
|
|
268
|
+
|
|
269
|
+
const sample = c.sample();
|
|
270
|
+
expect(sample.downKeys.size).toBe(0);
|
|
271
|
+
expect(sample.upKeys.size).toBe(0);
|
|
272
|
+
expect(sample.pressedKeys).toBeUndefined();
|
|
273
|
+
expect(sample.buttons).toEqual([false, false, false]);
|
|
274
|
+
expect(sample.pressedButtons).toBeUndefined();
|
|
275
|
+
expect(sample.releasedButtons).toBeUndefined();
|
|
276
|
+
});
|
|
277
|
+
|
|
260
278
|
it('forwards setPointerLockAllowed and detach to inner', () => {
|
|
261
279
|
let allowed: boolean | undefined;
|
|
262
280
|
let detached = false;
|
|
@@ -47,6 +47,25 @@ describe('keyboard snapshot edges', () => {
|
|
|
47
47
|
expect(snap.keyboard.downCode('KeyA')).toBe(true);
|
|
48
48
|
expect(snap.keyboard.justPressedCode('KeyA')).toBe(true);
|
|
49
49
|
});
|
|
50
|
+
|
|
51
|
+
it('uses producer-latched press edges even when held state is already released', () => {
|
|
52
|
+
const snap = snapshotFromSample({
|
|
53
|
+
...sample([], ['a'], [], ['KeyA']),
|
|
54
|
+
pressedKeys: new Set(['a']),
|
|
55
|
+
pressedCodes: new Set(['KeyA']),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(snap.keyboard.down('a')).toBe(false);
|
|
59
|
+
expect(snap.keyboard.up('a')).toBe(true);
|
|
60
|
+
expect(snap.keyboard.justPressed('a')).toBe(true);
|
|
61
|
+
expect(snap.keyboard.downCode('KeyA')).toBe(false);
|
|
62
|
+
expect(snap.keyboard.upCode('KeyA')).toBe(true);
|
|
63
|
+
expect(snap.keyboard.justPressedCode('KeyA')).toBe(true);
|
|
64
|
+
|
|
65
|
+
const next = snapshotFromSample(sample([]), undefined, undefined, snap);
|
|
66
|
+
expect(next.keyboard.justPressed('a')).toBe(false);
|
|
67
|
+
expect(next.keyboard.up('a')).toBe(false);
|
|
68
|
+
});
|
|
50
69
|
});
|
|
51
70
|
|
|
52
71
|
describe('empty backend sample ownership', () => {
|
|
@@ -93,4 +112,20 @@ describe('mouse snapshot edges', () => {
|
|
|
93
112
|
expect(released.mouse.button(0)).toBe(false);
|
|
94
113
|
expect(released.mouse.justReleased(0)).toBe(true);
|
|
95
114
|
});
|
|
115
|
+
|
|
116
|
+
it('preserves a producer-latched button tap between frame-start scans', () => {
|
|
117
|
+
const tap = snapshotFromSample({
|
|
118
|
+
...sample([]),
|
|
119
|
+
pressedButtons: [true, false, false],
|
|
120
|
+
releasedButtons: [true, false, false],
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(tap.mouse.button(0)).toBe(false);
|
|
124
|
+
expect(tap.mouse.justPressed(0)).toBe(true);
|
|
125
|
+
expect(tap.mouse.justReleased(0)).toBe(true);
|
|
126
|
+
|
|
127
|
+
const next = snapshotFromSample(sample([]), undefined, undefined, tap);
|
|
128
|
+
expect(next.mouse.justPressed(0)).toBe(false);
|
|
129
|
+
expect(next.mouse.justReleased(0)).toBe(false);
|
|
130
|
+
});
|
|
96
131
|
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { userEvent } from 'vitest/browser';
|
|
3
|
+
import { attachBrowserInputBackend } from '../browser-backend';
|
|
4
|
+
|
|
5
|
+
describe('pointer lock recovery (real Chromium)', () => {
|
|
6
|
+
it('attempts a trusted click even when hasFocus is false and contains rejection/error events', async () => {
|
|
7
|
+
const canvas = document.createElement('canvas');
|
|
8
|
+
canvas.dataset.testid = 'pointer-lock-target';
|
|
9
|
+
document.body.append(canvas);
|
|
10
|
+
const originalFocus = Object.getOwnPropertyDescriptor(document, 'hasFocus');
|
|
11
|
+
const originalRequest = Object.getOwnPropertyDescriptor(canvas, 'requestPointerLock');
|
|
12
|
+
const errors: Array<{ path: 'w3c' | 'provider'; cause: unknown }> = [];
|
|
13
|
+
const unhandled: unknown[] = [];
|
|
14
|
+
let requestCalls = 0;
|
|
15
|
+
const rejection = new DOMException('Pointer lock denied', 'NotAllowedError');
|
|
16
|
+
Object.defineProperty(document, 'hasFocus', {
|
|
17
|
+
configurable: true,
|
|
18
|
+
value: () => false,
|
|
19
|
+
});
|
|
20
|
+
Object.defineProperty(canvas, 'requestPointerLock', {
|
|
21
|
+
configurable: true,
|
|
22
|
+
value: () => {
|
|
23
|
+
requestCalls += 1;
|
|
24
|
+
return Promise.reject(rejection);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
const onUnhandled = (event: PromiseRejectionEvent): void => {
|
|
28
|
+
event.preventDefault();
|
|
29
|
+
unhandled.push(event.reason);
|
|
30
|
+
};
|
|
31
|
+
window.addEventListener('unhandledrejection', onUnhandled);
|
|
32
|
+
const handle = attachBrowserInputBackend(canvas, {
|
|
33
|
+
onLockError: (detail) => errors.push(detail),
|
|
34
|
+
});
|
|
35
|
+
try {
|
|
36
|
+
// userEvent delegates to Playwright's locator click, preserving the
|
|
37
|
+
// trusted user-activation boundary required by Pointer Lock.
|
|
38
|
+
await userEvent.click(canvas);
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
40
|
+
expect(requestCalls).toBe(1);
|
|
41
|
+
expect(errors[0]).toMatchObject({ path: 'w3c', cause: rejection });
|
|
42
|
+
expect(unhandled).toEqual([]);
|
|
43
|
+
|
|
44
|
+
const browserEvent = new Event('pointerlockerror');
|
|
45
|
+
document.dispatchEvent(browserEvent);
|
|
46
|
+
expect(errors[1]).toMatchObject({ path: 'w3c', cause: browserEvent });
|
|
47
|
+
expect(handle.backend.sample().pointerLocked).toBe(false);
|
|
48
|
+
} finally {
|
|
49
|
+
handle();
|
|
50
|
+
window.removeEventListener('unhandledrejection', onUnhandled);
|
|
51
|
+
if (originalFocus === undefined) delete (document as { hasFocus?: unknown }).hasFocus;
|
|
52
|
+
else Object.defineProperty(document, 'hasFocus', originalFocus);
|
|
53
|
+
if (originalRequest === undefined)
|
|
54
|
+
delete (canvas as { requestPointerLock?: unknown }).requestPointerLock;
|
|
55
|
+
else Object.defineProperty(canvas, 'requestPointerLock', originalRequest);
|
|
56
|
+
canvas.remove();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|