@prosopo/procaptcha-puzzle 2.10.40 → 2.10.41
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/.turbo/turbo-build$colon$cjs.log +4 -4
- package/.turbo/turbo-build$colon$tsc.log +19 -19
- package/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +7 -0
- package/dist/cjs/components/ProcaptchaWidget.cjs +10 -5
- package/dist/cjs/services/Manager.cjs +3 -3
- package/dist/components/ProcaptchaWidget.d.ts.map +1 -1
- package/dist/components/ProcaptchaWidget.js +10 -5
- package/dist/components/ProcaptchaWidget.js.map +1 -1
- package/dist/services/Manager.d.ts.map +1 -1
- package/dist/services/Manager.js +3 -3
- package/dist/services/Manager.js.map +1 -1
- package/dist/tests/manager.unit.test.d.ts +2 -0
- package/dist/tests/manager.unit.test.d.ts.map +1 -0
- package/dist/tests/manager.unit.test.js +506 -0
- package/dist/tests/manager.unit.test.js.map +1 -0
- package/dist/tests/managerHarness.d.ts +20 -0
- package/dist/tests/managerHarness.d.ts.map +1 -0
- package/dist/tests/managerHarness.js +86 -0
- package/dist/tests/managerHarness.js.map +1 -0
- package/dist/tests/procaptchaPuzzle.test-d.d.ts +2 -0
- package/dist/tests/procaptchaPuzzle.test-d.d.ts.map +1 -0
- package/dist/tests/procaptchaPuzzle.test-d.js +97 -0
- package/dist/tests/procaptchaPuzzle.test-d.js.map +1 -0
- package/dist/tests/procaptchaPuzzle.unit.test.d.ts +2 -0
- package/dist/tests/procaptchaPuzzle.unit.test.d.ts.map +1 -0
- package/dist/tests/procaptchaPuzzle.unit.test.js +81 -0
- package/dist/tests/procaptchaPuzzle.unit.test.js.map +1 -0
- package/dist/tests/procaptchaWidget.unit.test.d.ts +2 -0
- package/dist/tests/procaptchaWidget.unit.test.d.ts.map +1 -0
- package/dist/tests/procaptchaWidget.unit.test.js +547 -0
- package/dist/tests/procaptchaWidget.unit.test.js.map +1 -0
- package/dist/tests/puzzleCanvas.unit.test.d.ts +2 -0
- package/dist/tests/puzzleCanvas.unit.test.d.ts.map +1 -0
- package/dist/tests/puzzleCanvas.unit.test.js +312 -0
- package/dist/tests/puzzleCanvas.unit.test.js.map +1 -0
- package/dist/tests/setup.d.ts +5 -0
- package/dist/tests/setup.d.ts.map +1 -0
- package/dist/tests/setup.js +3 -0
- package/dist/tests/setup.js.map +1 -0
- package/package.json +9 -4
- package/src/components/ProcaptchaWidget.tsx +17 -6
- package/src/services/Manager.ts +10 -6
- package/src/tests/manager.unit.test.ts +763 -0
- package/src/tests/managerHarness.ts +169 -0
- package/src/tests/procaptchaPuzzle.test-d.ts +191 -0
- package/src/tests/procaptchaPuzzle.unit.test.ts +123 -0
- package/src/tests/procaptchaWidget.unit.test.ts +789 -0
- package/src/tests/puzzleCanvas.unit.test.ts +418 -0
- package/src/tests/setup.ts +26 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vite.test.config.ts +27 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
import type { PuzzleEvent } from "@prosopo/types";
|
|
16
|
+
import { type ReactElement, act, createElement } from "react";
|
|
17
|
+
import { type Root, createRoot } from "react-dom/client";
|
|
18
|
+
import {
|
|
19
|
+
type Mock,
|
|
20
|
+
afterEach,
|
|
21
|
+
beforeEach,
|
|
22
|
+
describe,
|
|
23
|
+
expect,
|
|
24
|
+
test,
|
|
25
|
+
vi,
|
|
26
|
+
} from "vitest";
|
|
27
|
+
import { PuzzleCanvas } from "../components/PuzzleCanvas.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The canvas is the only piece of the puzzle flow the user actually touches:
|
|
31
|
+
* it owns the drag, the clamping that keeps the piece inside the board, and the
|
|
32
|
+
* trail of positions the provider scores. Every test drives real DOM events
|
|
33
|
+
* against a real render rather than calling the handlers directly.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const CONTAINER_WIDTH = 300;
|
|
37
|
+
const CONTAINER_HEIGHT = 200;
|
|
38
|
+
const PIECE_SIZE = 24;
|
|
39
|
+
|
|
40
|
+
interface CanvasProps {
|
|
41
|
+
originX: number;
|
|
42
|
+
originY: number;
|
|
43
|
+
targetX: number;
|
|
44
|
+
targetY: number;
|
|
45
|
+
onComplete: Mock<
|
|
46
|
+
(finalX: number, finalY: number, puzzleEvents: PuzzleEvent[]) => void
|
|
47
|
+
>;
|
|
48
|
+
showRetry: boolean;
|
|
49
|
+
submitting: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let container: HTMLDivElement;
|
|
53
|
+
let root: Root;
|
|
54
|
+
let onComplete: Mock<
|
|
55
|
+
(finalX: number, finalY: number, puzzleEvents: PuzzleEvent[]) => void
|
|
56
|
+
>;
|
|
57
|
+
|
|
58
|
+
const props = (overrides: Partial<CanvasProps> = {}): CanvasProps => ({
|
|
59
|
+
originX: 20,
|
|
60
|
+
originY: 100,
|
|
61
|
+
targetX: 200,
|
|
62
|
+
targetY: 80,
|
|
63
|
+
onComplete,
|
|
64
|
+
showRetry: false,
|
|
65
|
+
submitting: false,
|
|
66
|
+
...overrides,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const render = (canvasProps: CanvasProps): void => {
|
|
70
|
+
act(() => {
|
|
71
|
+
root.render(createElement(PuzzleCanvas, canvasProps) as ReactElement);
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const piece = (): HTMLElement => {
|
|
76
|
+
const element = container.querySelector<HTMLElement>(
|
|
77
|
+
'[data-cy="prosopo-puzzle-piece"]',
|
|
78
|
+
);
|
|
79
|
+
if (!element) throw new Error("expected the puzzle piece to be rendered");
|
|
80
|
+
return element;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** The piece is positioned by its centre, so undo the offset the style adds. */
|
|
84
|
+
const piecePosition = (): { x: number; y: number } => ({
|
|
85
|
+
x: Number.parseFloat(piece().style.left) + PIECE_SIZE / 2,
|
|
86
|
+
y: Number.parseFloat(piece().style.top) + PIECE_SIZE / 2,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const mouseDown = (clientX: number, clientY: number): void => {
|
|
90
|
+
act(() => {
|
|
91
|
+
piece().dispatchEvent(
|
|
92
|
+
new MouseEvent("mousedown", { bubbles: true, clientX, clientY }),
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const mouseMove = (clientX: number, clientY: number): void => {
|
|
98
|
+
act(() => {
|
|
99
|
+
document.dispatchEvent(
|
|
100
|
+
new MouseEvent("mousemove", { bubbles: true, clientX, clientY }),
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const mouseUp = (): void => {
|
|
106
|
+
act(() => {
|
|
107
|
+
document.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* jsdom has no touch constructors, so the event carries a plain object shaped
|
|
113
|
+
* like the single Touch the component reads.
|
|
114
|
+
*/
|
|
115
|
+
const touchEvent = (
|
|
116
|
+
type: string,
|
|
117
|
+
touches: { clientX: number; clientY: number }[],
|
|
118
|
+
): Event => {
|
|
119
|
+
const event = new Event(type, { bubbles: true });
|
|
120
|
+
Object.defineProperty(event, "touches", { value: touches });
|
|
121
|
+
return event;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const touchStart = (touches: { clientX: number; clientY: number }[]): void => {
|
|
125
|
+
act(() => {
|
|
126
|
+
piece().dispatchEvent(touchEvent("touchstart", touches));
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const touchMove = (touches: { clientX: number; clientY: number }[]): void => {
|
|
131
|
+
act(() => {
|
|
132
|
+
document.dispatchEvent(touchEvent("touchmove", touches));
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const touchEnd = (): void => {
|
|
137
|
+
act(() => {
|
|
138
|
+
document.dispatchEvent(touchEvent("touchend", []));
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
beforeEach(() => {
|
|
143
|
+
onComplete =
|
|
144
|
+
vi.fn<
|
|
145
|
+
(finalX: number, finalY: number, puzzleEvents: PuzzleEvent[]) => void
|
|
146
|
+
>();
|
|
147
|
+
container = document.createElement("div");
|
|
148
|
+
document.body.appendChild(container);
|
|
149
|
+
act(() => {
|
|
150
|
+
root = createRoot(container);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
afterEach(() => {
|
|
155
|
+
act(() => {
|
|
156
|
+
root.unmount();
|
|
157
|
+
});
|
|
158
|
+
container.remove();
|
|
159
|
+
vi.useRealTimers();
|
|
160
|
+
vi.restoreAllMocks();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("what it puts on screen", () => {
|
|
164
|
+
test("the piece starts at the origin the challenge named", () => {
|
|
165
|
+
render(props());
|
|
166
|
+
expect(piecePosition()).toEqual({ x: 20, y: 100 });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("a new challenge moves the piece back to its new origin", () => {
|
|
170
|
+
render(props());
|
|
171
|
+
mouseDown(20, 100);
|
|
172
|
+
mouseMove(120, 120);
|
|
173
|
+
render(props({ originX: 50, originY: 60 }));
|
|
174
|
+
expect(piecePosition()).toEqual({ x: 50, y: 60 });
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("re-rendering with the same origin leaves a dragged piece alone", () => {
|
|
178
|
+
render(props());
|
|
179
|
+
mouseDown(20, 100);
|
|
180
|
+
mouseMove(120, 120);
|
|
181
|
+
render(props());
|
|
182
|
+
expect(piecePosition()).toEqual({ x: 120, y: 120 });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("the first go asks the user to drag the piece", () => {
|
|
186
|
+
render(props());
|
|
187
|
+
expect(container.textContent).toContain("Drag the piece to the target");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("a retry says so instead", () => {
|
|
191
|
+
render(props({ showRetry: true }));
|
|
192
|
+
expect(container.textContent).toContain("Not quite");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("the piece cannot be grabbed while a solution is in flight", () => {
|
|
196
|
+
render(props({ submitting: true }));
|
|
197
|
+
mouseDown(20, 100);
|
|
198
|
+
mouseMove(120, 120);
|
|
199
|
+
mouseUp();
|
|
200
|
+
expect(piecePosition()).toEqual({ x: 20, y: 100 });
|
|
201
|
+
expect(onComplete).not.toHaveBeenCalled();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("a tap cannot be started while a solution is in flight either", () => {
|
|
205
|
+
render(props({ submitting: true }));
|
|
206
|
+
touchStart([{ clientX: 20, clientY: 100 }]);
|
|
207
|
+
touchMove([{ clientX: 120, clientY: 120 }]);
|
|
208
|
+
touchEnd();
|
|
209
|
+
expect(onComplete).not.toHaveBeenCalled();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("the shake on a retry stops on its own", () => {
|
|
213
|
+
vi.useFakeTimers();
|
|
214
|
+
render(props({ showRetry: true }));
|
|
215
|
+
act(() => {
|
|
216
|
+
vi.advanceTimersByTime(600);
|
|
217
|
+
});
|
|
218
|
+
// Nothing to assert beyond survival: the timer fires into a live
|
|
219
|
+
// component rather than leaking past the shake.
|
|
220
|
+
expect(piecePosition()).toEqual({ x: 20, y: 100 });
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("unmounting mid-shake cancels the timer", () => {
|
|
224
|
+
vi.useFakeTimers();
|
|
225
|
+
render(props({ showRetry: true }));
|
|
226
|
+
act(() => {
|
|
227
|
+
root.unmount();
|
|
228
|
+
});
|
|
229
|
+
act(() => {
|
|
230
|
+
vi.advanceTimersByTime(600);
|
|
231
|
+
});
|
|
232
|
+
act(() => {
|
|
233
|
+
root = createRoot(container);
|
|
234
|
+
});
|
|
235
|
+
expect(
|
|
236
|
+
container.querySelector('[data-cy="prosopo-puzzle-piece"]'),
|
|
237
|
+
).toBeNull();
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe("dragging with a mouse", () => {
|
|
242
|
+
test("the piece follows the pointer", () => {
|
|
243
|
+
render(props());
|
|
244
|
+
mouseDown(20, 100);
|
|
245
|
+
mouseMove(150, 90);
|
|
246
|
+
expect(piecePosition()).toEqual({ x: 150, y: 90 });
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("grabbing the piece off-centre keeps the offset", () => {
|
|
250
|
+
render(props());
|
|
251
|
+
// Grabbed 5px right of the centre, so the centre trails the pointer by 5.
|
|
252
|
+
mouseDown(25, 100);
|
|
253
|
+
mouseMove(150, 100);
|
|
254
|
+
expect(piecePosition()).toEqual({ x: 145, y: 100 });
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("a pointer moving before the piece is grabbed is ignored", () => {
|
|
258
|
+
render(props());
|
|
259
|
+
mouseMove(150, 90);
|
|
260
|
+
expect(piecePosition()).toEqual({ x: 20, y: 100 });
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("the piece cannot be dragged off the left or top of the board", () => {
|
|
264
|
+
render(props());
|
|
265
|
+
mouseDown(20, 100);
|
|
266
|
+
mouseMove(-500, -500);
|
|
267
|
+
expect(piecePosition()).toEqual({ x: 0, y: 0 });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("nor off the right or bottom", () => {
|
|
271
|
+
render(props());
|
|
272
|
+
mouseDown(20, 100);
|
|
273
|
+
mouseMove(5000, 5000);
|
|
274
|
+
expect(piecePosition()).toEqual({
|
|
275
|
+
x: CONTAINER_WIDTH,
|
|
276
|
+
y: CONTAINER_HEIGHT,
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("letting go reports where the piece landed, with the trail", () => {
|
|
281
|
+
render(props());
|
|
282
|
+
mouseDown(20, 100);
|
|
283
|
+
mouseMove(100, 95);
|
|
284
|
+
mouseMove(200, 80);
|
|
285
|
+
mouseUp();
|
|
286
|
+
expect(onComplete).toHaveBeenCalledTimes(1);
|
|
287
|
+
const [finalX, finalY, events] = onComplete.mock.calls[0] ?? [];
|
|
288
|
+
expect(finalX).toBe(200);
|
|
289
|
+
expect(finalY).toBe(80);
|
|
290
|
+
expect(events?.map((event) => [event.x, event.y])).toEqual([
|
|
291
|
+
[100, 95],
|
|
292
|
+
[200, 80],
|
|
293
|
+
]);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("a grab released without moving reports the origin and no trail", () => {
|
|
297
|
+
render(props());
|
|
298
|
+
mouseDown(20, 100);
|
|
299
|
+
mouseUp();
|
|
300
|
+
expect(onComplete).toHaveBeenCalledWith(20, 100, []);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("letting go without having grabbed anything reports nothing", () => {
|
|
304
|
+
render(props());
|
|
305
|
+
mouseUp();
|
|
306
|
+
expect(onComplete).not.toHaveBeenCalled();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("a second release after the drop is ignored", () => {
|
|
310
|
+
render(props());
|
|
311
|
+
mouseDown(20, 100);
|
|
312
|
+
mouseMove(200, 80);
|
|
313
|
+
mouseUp();
|
|
314
|
+
mouseUp();
|
|
315
|
+
expect(onComplete).toHaveBeenCalledTimes(1);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("moving after the drop no longer moves the piece", () => {
|
|
319
|
+
render(props());
|
|
320
|
+
mouseDown(20, 100);
|
|
321
|
+
mouseMove(200, 80);
|
|
322
|
+
mouseUp();
|
|
323
|
+
mouseMove(50, 50);
|
|
324
|
+
expect(piecePosition()).toEqual({ x: 200, y: 80 });
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("a second drag starts from a clean trail", () => {
|
|
328
|
+
render(props());
|
|
329
|
+
mouseDown(20, 100);
|
|
330
|
+
mouseMove(100, 95);
|
|
331
|
+
mouseUp();
|
|
332
|
+
mouseDown(100, 95);
|
|
333
|
+
mouseMove(200, 80);
|
|
334
|
+
mouseUp();
|
|
335
|
+
const events = onComplete.mock.calls[1]?.[2];
|
|
336
|
+
expect(events?.map((event) => [event.x, event.y])).toEqual([[200, 80]]);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("the trail is timestamped in order", () => {
|
|
340
|
+
render(props());
|
|
341
|
+
mouseDown(20, 100);
|
|
342
|
+
mouseMove(100, 95);
|
|
343
|
+
mouseMove(200, 80);
|
|
344
|
+
mouseUp();
|
|
345
|
+
const events = onComplete.mock.calls[0]?.[2] ?? [];
|
|
346
|
+
expect(events).toHaveLength(2);
|
|
347
|
+
expect(events[1]?.t).toBeGreaterThanOrEqual(events[0]?.t ?? 0);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("the trail handed over is a copy, safe from the next drag", () => {
|
|
351
|
+
render(props());
|
|
352
|
+
mouseDown(20, 100);
|
|
353
|
+
mouseMove(200, 80);
|
|
354
|
+
mouseUp();
|
|
355
|
+
const events = onComplete.mock.calls[0]?.[2] ?? [];
|
|
356
|
+
mouseDown(200, 80);
|
|
357
|
+
mouseMove(10, 10);
|
|
358
|
+
expect(events).toHaveLength(1);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
describe("dragging with a finger", () => {
|
|
363
|
+
test("the piece follows the touch", () => {
|
|
364
|
+
render(props());
|
|
365
|
+
touchStart([{ clientX: 20, clientY: 100 }]);
|
|
366
|
+
touchMove([{ clientX: 150, clientY: 90 }]);
|
|
367
|
+
expect(piecePosition()).toEqual({ x: 150, y: 90 });
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("lifting the finger reports the drop", () => {
|
|
371
|
+
render(props());
|
|
372
|
+
touchStart([{ clientX: 20, clientY: 100 }]);
|
|
373
|
+
touchMove([{ clientX: 200, clientY: 80 }]);
|
|
374
|
+
touchEnd();
|
|
375
|
+
expect(onComplete).toHaveBeenCalledWith(200, 80, [
|
|
376
|
+
expect.objectContaining({ x: 200, y: 80 }),
|
|
377
|
+
]);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("a touchstart carrying no touches does not start a drag", () => {
|
|
381
|
+
render(props());
|
|
382
|
+
touchStart([]);
|
|
383
|
+
touchMove([{ clientX: 150, clientY: 90 }]);
|
|
384
|
+
touchEnd();
|
|
385
|
+
expect(piecePosition()).toEqual({ x: 20, y: 100 });
|
|
386
|
+
expect(onComplete).not.toHaveBeenCalled();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("a touchmove carrying no touches is ignored mid-drag", () => {
|
|
390
|
+
render(props());
|
|
391
|
+
touchStart([{ clientX: 20, clientY: 100 }]);
|
|
392
|
+
touchMove([]);
|
|
393
|
+
expect(piecePosition()).toEqual({ x: 20, y: 100 });
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("a finger drag is clamped to the board like a mouse drag", () => {
|
|
397
|
+
render(props());
|
|
398
|
+
touchStart([{ clientX: 20, clientY: 100 }]);
|
|
399
|
+
touchMove([{ clientX: 9999, clientY: -9999 }]);
|
|
400
|
+
expect(piecePosition()).toEqual({ x: CONTAINER_WIDTH, y: 0 });
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
describe("after it goes away", () => {
|
|
405
|
+
test("its document listeners go with it", () => {
|
|
406
|
+
render(props());
|
|
407
|
+
mouseDown(20, 100);
|
|
408
|
+
act(() => {
|
|
409
|
+
root.unmount();
|
|
410
|
+
});
|
|
411
|
+
mouseMove(150, 90);
|
|
412
|
+
mouseUp();
|
|
413
|
+
expect(onComplete).not.toHaveBeenCalled();
|
|
414
|
+
act(() => {
|
|
415
|
+
root = createRoot(container);
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
// React only flushes work synchronously inside act() when it believes it is
|
|
16
|
+
// under test; without this flag every render lands after the assertion and the
|
|
17
|
+
// container reads as empty.
|
|
18
|
+
// React reads this off the global object rather than an import, so it has to
|
|
19
|
+
// be declared before it can be set without widening globalThis to any.
|
|
20
|
+
declare global {
|
|
21
|
+
var IS_REACT_ACT_ENVIRONMENT: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
25
|
+
|
|
26
|
+
export {};
|