@prosopo/procaptcha-puzzle 2.13.3 → 2.14.1
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 +6 -6
- package/.turbo/turbo-build$colon$tsc.log +20 -20
- package/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +66 -0
- package/dist/cjs/components/procaptchaPuzzle.cjs +22 -0
- package/dist/cjs/components/procaptchaWidget.cjs +225 -0
- package/dist/cjs/components/puzzleCanvas.cjs +451 -0
- package/dist/cjs/index.cjs +7 -2
- package/dist/components/procaptchaPuzzle.d.ts +7 -0
- package/dist/components/procaptchaPuzzle.d.ts.map +1 -0
- package/dist/components/procaptchaPuzzle.js +21 -0
- package/dist/components/procaptchaPuzzle.js.map +1 -0
- package/dist/components/procaptchaWidget.d.ts +7 -0
- package/dist/components/procaptchaWidget.d.ts.map +1 -0
- package/dist/components/procaptchaWidget.js +225 -0
- package/dist/components/procaptchaWidget.js.map +1 -0
- package/dist/components/{PuzzleCanvas.d.ts → puzzleCanvas.d.ts} +6 -4
- package/dist/components/puzzleCanvas.d.ts.map +1 -0
- package/dist/components/puzzleCanvas.js +451 -0
- package/dist/components/puzzleCanvas.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/tests/domHarness.d.ts +20 -0
- package/dist/tests/domHarness.d.ts.map +1 -0
- package/dist/tests/domHarness.js +56 -0
- package/dist/tests/domHarness.js.map +1 -0
- package/dist/tests/invisibleCanvasVisibility.unit.test.d.ts +2 -0
- package/dist/tests/invisibleCanvasVisibility.unit.test.d.ts.map +1 -0
- package/dist/tests/invisibleCanvasVisibility.unit.test.js +102 -0
- package/dist/tests/invisibleCanvasVisibility.unit.test.js.map +1 -0
- package/dist/tests/procaptchaPuzzle.test-d.js +13 -10
- package/dist/tests/procaptchaPuzzle.test-d.js.map +1 -1
- package/dist/tests/procaptchaPuzzle.unit.test.js +44 -33
- package/dist/tests/procaptchaPuzzle.unit.test.js.map +1 -1
- package/dist/tests/procaptchaWidget.unit.test.js +108 -175
- package/dist/tests/procaptchaWidget.unit.test.js.map +1 -1
- package/dist/tests/puzzleCanvas.unit.test.js +56 -82
- package/dist/tests/puzzleCanvas.unit.test.js.map +1 -1
- package/package.json +10 -14
- package/src/components/procaptchaPuzzle.ts +54 -0
- package/src/components/procaptchaWidget.ts +392 -0
- package/src/components/puzzleCanvas.ts +658 -0
- package/src/index.ts +3 -2
- package/src/tests/domHarness.ts +122 -0
- package/src/tests/invisibleCanvasVisibility.unit.test.ts +179 -0
- package/src/tests/procaptchaPuzzle.test-d.ts +33 -17
- package/src/tests/procaptchaPuzzle.unit.test.ts +59 -44
- package/src/tests/procaptchaWidget.unit.test.ts +145 -258
- package/src/tests/puzzleCanvas.unit.test.ts +94 -122
- package/tsconfig.cjs.json +1 -2
- package/tsconfig.json +1 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/vite.test.config.ts +3 -4
- package/dist/cjs/components/ProcaptchaPuzzle.cjs +0 -7
- package/dist/cjs/components/ProcaptchaWidget.cjs +0 -206
- package/dist/cjs/components/PuzzleCanvas.cjs +0 -476
- package/dist/components/ProcaptchaPuzzle.d.ts +0 -3
- package/dist/components/ProcaptchaPuzzle.d.ts.map +0 -1
- package/dist/components/ProcaptchaPuzzle.js +0 -7
- package/dist/components/ProcaptchaPuzzle.js.map +0 -1
- package/dist/components/ProcaptchaWidget.d.ts +0 -4
- package/dist/components/ProcaptchaWidget.d.ts.map +0 -1
- package/dist/components/ProcaptchaWidget.js +0 -207
- package/dist/components/ProcaptchaWidget.js.map +0 -1
- package/dist/components/PuzzleCanvas.d.ts.map +0 -1
- package/dist/components/PuzzleCanvas.js +0 -476
- package/dist/components/PuzzleCanvas.js.map +0 -1
- package/dist/tests/setup.d.ts +0 -5
- package/dist/tests/setup.d.ts.map +0 -1
- package/dist/tests/setup.js +0 -3
- package/dist/tests/setup.js.map +0 -1
- package/src/components/ProcaptchaPuzzle.tsx +0 -36
- package/src/components/ProcaptchaWidget.tsx +0 -320
- package/src/components/PuzzleCanvas.tsx +0 -638
- package/src/tests/setup.ts +0 -26
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
// See the License for the specific language governing permissions and
|
|
13
13
|
// limitations under the License.
|
|
14
14
|
|
|
15
|
+
import type { Translator } from "@prosopo/locale";
|
|
16
|
+
import type { Component } from "@prosopo/procaptcha-common";
|
|
15
17
|
import type { PuzzleEvent } from "@prosopo/types";
|
|
16
|
-
import {
|
|
17
|
-
import { type ReactElement, act, createElement } from "react";
|
|
18
|
-
import { type Root, createRoot } from "react-dom/client";
|
|
18
|
+
import { lightTheme } from "@prosopo/widget-skeleton";
|
|
19
19
|
import {
|
|
20
20
|
type Mock,
|
|
21
21
|
afterEach,
|
|
@@ -25,7 +25,10 @@ import {
|
|
|
25
25
|
test,
|
|
26
26
|
vi,
|
|
27
27
|
} from "vitest";
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
type PuzzleCanvasProps,
|
|
30
|
+
mountPuzzleCanvas,
|
|
31
|
+
} from "../components/puzzleCanvas.js";
|
|
29
32
|
|
|
30
33
|
/**
|
|
31
34
|
* The canvas is the only piece of the puzzle flow the user actually touches:
|
|
@@ -34,73 +37,68 @@ import { PuzzleCanvas } from "../components/PuzzleCanvas.js";
|
|
|
34
37
|
* against a real render rather than calling the handlers directly.
|
|
35
38
|
*/
|
|
36
39
|
|
|
40
|
+
const CONTAINER_WIDTH = 300;
|
|
41
|
+
const CONTAINER_HEIGHT = 200;
|
|
42
|
+
const PIECE_SIZE = 44;
|
|
43
|
+
|
|
37
44
|
/**
|
|
38
|
-
* The real
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
45
|
+
* The real translator reaches for an http backend the moment it is asked for a
|
|
46
|
+
* string, which jsdom refuses. The English defaults the canvas ships stand in
|
|
47
|
+
* instead, interpolated the way i18next would, so the assertions below read as
|
|
48
|
+
* the copy a user is actually given.
|
|
42
49
|
*/
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
options?: { defaultValue?: string } & Record<string, unknown>,
|
|
48
|
-
): string =>
|
|
49
|
-
(options?.defaultValue ?? key).replace(
|
|
50
|
+
const translator = (): Translator => ({
|
|
51
|
+
t: (key: string, options?: Record<string, unknown>): string => {
|
|
52
|
+
const template = (options?.defaultValue as string | undefined) ?? key;
|
|
53
|
+
return template.replace(
|
|
50
54
|
/{{(\w+)}}/g,
|
|
51
55
|
(placeholder: string, name: string) =>
|
|
52
56
|
options && name in options ? String(options[name]) : placeholder,
|
|
53
57
|
);
|
|
54
|
-
|
|
58
|
+
},
|
|
59
|
+
isReady: () => true,
|
|
60
|
+
subscribe: () => () => undefined,
|
|
61
|
+
i18n: {} as Translator["i18n"],
|
|
55
62
|
});
|
|
56
63
|
|
|
57
|
-
|
|
58
|
-
const CONTAINER_HEIGHT = 200;
|
|
59
|
-
const PIECE_SIZE = 44;
|
|
60
|
-
|
|
61
|
-
interface CanvasProps {
|
|
62
|
-
originX: number;
|
|
63
|
-
originY: number;
|
|
64
|
-
background: string;
|
|
65
|
-
piece: string;
|
|
66
|
-
pieceSize: number;
|
|
67
|
-
onComplete: Mock<
|
|
68
|
-
(finalX: number, finalY: number, puzzleEvents: PuzzleEvent[]) => void
|
|
69
|
-
>;
|
|
70
|
-
showRetry: boolean;
|
|
71
|
-
submitting: boolean;
|
|
72
|
-
theme: Theme;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
let container: HTMLDivElement;
|
|
76
|
-
let root: Root;
|
|
64
|
+
let canvas: Component<PuzzleCanvasProps> | undefined;
|
|
77
65
|
let onComplete: Mock<
|
|
78
66
|
(finalX: number, finalY: number, puzzleEvents: PuzzleEvent[]) => void
|
|
79
67
|
>;
|
|
80
68
|
|
|
81
|
-
const props = (
|
|
69
|
+
const props = (
|
|
70
|
+
overrides: Partial<PuzzleCanvasProps> = {},
|
|
71
|
+
): PuzzleCanvasProps => ({
|
|
82
72
|
originX: 20,
|
|
83
73
|
originY: 100,
|
|
84
74
|
background: "data:image/webp;base64,UklGRg==",
|
|
85
75
|
piece: "data:image/webp;base64,UklGRg==",
|
|
86
|
-
pieceSize:
|
|
76
|
+
pieceSize: PIECE_SIZE,
|
|
87
77
|
onComplete,
|
|
88
78
|
showRetry: false,
|
|
89
79
|
submitting: false,
|
|
90
80
|
theme: lightTheme,
|
|
81
|
+
translator: translator(),
|
|
91
82
|
...overrides,
|
|
92
83
|
});
|
|
93
84
|
|
|
94
|
-
const render = (canvasProps:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
85
|
+
const render = (canvasProps: PuzzleCanvasProps): void => {
|
|
86
|
+
if (canvas) {
|
|
87
|
+
canvas.update(canvasProps);
|
|
88
|
+
} else {
|
|
89
|
+
canvas = mountPuzzleCanvas(canvasProps);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const destroy = (): void => {
|
|
94
|
+
canvas?.destroy();
|
|
95
|
+
canvas = undefined;
|
|
98
96
|
};
|
|
99
97
|
|
|
100
98
|
/**
|
|
101
|
-
* The canvas
|
|
102
|
-
*
|
|
103
|
-
*
|
|
99
|
+
* The canvas puts itself on the body — it has to escape the query container
|
|
100
|
+
* the widget skeleton wraps it in — so everything that reads the rendered
|
|
101
|
+
* output reads the body.
|
|
104
102
|
*/
|
|
105
103
|
const overlay = (): HTMLElement => document.body;
|
|
106
104
|
|
|
@@ -119,25 +117,19 @@ const piecePosition = (): { x: number; y: number } => ({
|
|
|
119
117
|
});
|
|
120
118
|
|
|
121
119
|
const mouseDown = (clientX: number, clientY: number): void => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
);
|
|
126
|
-
});
|
|
120
|
+
piece().dispatchEvent(
|
|
121
|
+
new MouseEvent("mousedown", { bubbles: true, clientX, clientY }),
|
|
122
|
+
);
|
|
127
123
|
};
|
|
128
124
|
|
|
129
125
|
const mouseMove = (clientX: number, clientY: number): void => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
);
|
|
134
|
-
});
|
|
126
|
+
document.dispatchEvent(
|
|
127
|
+
new MouseEvent("mousemove", { bubbles: true, clientX, clientY }),
|
|
128
|
+
);
|
|
135
129
|
};
|
|
136
130
|
|
|
137
131
|
const mouseUp = (): void => {
|
|
138
|
-
|
|
139
|
-
document.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
|
|
140
|
-
});
|
|
132
|
+
document.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
|
|
141
133
|
};
|
|
142
134
|
|
|
143
135
|
/**
|
|
@@ -154,34 +146,26 @@ const touchEvent = (
|
|
|
154
146
|
};
|
|
155
147
|
|
|
156
148
|
const touchStart = (touches: { clientX: number; clientY: number }[]): void => {
|
|
157
|
-
|
|
158
|
-
piece().dispatchEvent(touchEvent("touchstart", touches));
|
|
159
|
-
});
|
|
149
|
+
piece().dispatchEvent(touchEvent("touchstart", touches));
|
|
160
150
|
};
|
|
161
151
|
|
|
162
152
|
const touchMove = (touches: { clientX: number; clientY: number }[]): void => {
|
|
163
|
-
|
|
164
|
-
document.dispatchEvent(touchEvent("touchmove", touches));
|
|
165
|
-
});
|
|
153
|
+
document.dispatchEvent(touchEvent("touchmove", touches));
|
|
166
154
|
};
|
|
167
155
|
|
|
168
156
|
const touchEnd = (): void => {
|
|
169
|
-
|
|
170
|
-
document.dispatchEvent(touchEvent("touchend", []));
|
|
171
|
-
});
|
|
157
|
+
document.dispatchEvent(touchEvent("touchend", []));
|
|
172
158
|
};
|
|
173
159
|
|
|
174
160
|
const keyDown = (key: string, options: { shiftKey?: boolean } = {}): void => {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
);
|
|
184
|
-
});
|
|
161
|
+
piece().dispatchEvent(
|
|
162
|
+
new KeyboardEvent("keydown", {
|
|
163
|
+
key,
|
|
164
|
+
bubbles: true,
|
|
165
|
+
cancelable: true,
|
|
166
|
+
shiftKey: options.shiftKey ?? false,
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
185
169
|
};
|
|
186
170
|
|
|
187
171
|
/**
|
|
@@ -222,18 +206,11 @@ beforeEach(() => {
|
|
|
222
206
|
vi.fn<
|
|
223
207
|
(finalX: number, finalY: number, puzzleEvents: PuzzleEvent[]) => void
|
|
224
208
|
>();
|
|
225
|
-
|
|
226
|
-
document.body.appendChild(container);
|
|
227
|
-
act(() => {
|
|
228
|
-
root = createRoot(container);
|
|
229
|
-
});
|
|
209
|
+
canvas = undefined;
|
|
230
210
|
});
|
|
231
211
|
|
|
232
212
|
afterEach(() => {
|
|
233
|
-
|
|
234
|
-
root.unmount();
|
|
235
|
-
});
|
|
236
|
-
container.remove();
|
|
213
|
+
destroy();
|
|
237
214
|
vi.useRealTimers();
|
|
238
215
|
vi.unstubAllGlobals();
|
|
239
216
|
vi.restoreAllMocks();
|
|
@@ -291,9 +268,7 @@ describe("what it puts on screen", () => {
|
|
|
291
268
|
test("the shake on a retry stops on its own", () => {
|
|
292
269
|
vi.useFakeTimers();
|
|
293
270
|
render(props({ showRetry: true }));
|
|
294
|
-
|
|
295
|
-
vi.advanceTimersByTime(600);
|
|
296
|
-
});
|
|
271
|
+
vi.advanceTimersByTime(600);
|
|
297
272
|
// Nothing to assert beyond survival: the timer fires into a live
|
|
298
273
|
// component rather than leaking past the shake.
|
|
299
274
|
expect(piecePosition()).toEqual({ x: 20, y: 100 });
|
|
@@ -302,15 +277,8 @@ describe("what it puts on screen", () => {
|
|
|
302
277
|
test("unmounting mid-shake cancels the timer", () => {
|
|
303
278
|
vi.useFakeTimers();
|
|
304
279
|
render(props({ showRetry: true }));
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
});
|
|
308
|
-
act(() => {
|
|
309
|
-
vi.advanceTimersByTime(600);
|
|
310
|
-
});
|
|
311
|
-
act(() => {
|
|
312
|
-
root = createRoot(container);
|
|
313
|
-
});
|
|
280
|
+
destroy();
|
|
281
|
+
vi.advanceTimersByTime(600);
|
|
314
282
|
expect(
|
|
315
283
|
overlay().querySelector('[data-cy="prosopo-puzzle-piece"]'),
|
|
316
284
|
).toBeNull();
|
|
@@ -366,17 +334,20 @@ describe("dragging with a mouse", () => {
|
|
|
366
334
|
const [finalX, finalY, events] = onComplete.mock.calls[0] ?? [];
|
|
367
335
|
expect(finalX).toBe(200);
|
|
368
336
|
expect(finalY).toBe(80);
|
|
369
|
-
expect(events?.map((event) => [event.x, event.y])).toEqual([
|
|
337
|
+
expect(events?.map((event: PuzzleEvent) => [event.x, event.y])).toEqual([
|
|
370
338
|
[100, 95],
|
|
371
339
|
[200, 80],
|
|
372
340
|
]);
|
|
373
341
|
});
|
|
374
342
|
|
|
375
|
-
|
|
343
|
+
// Reporting the origin here spent the challenge on a position that cannot be
|
|
344
|
+
// correct, and the provider rejects the replacement fetch — so a single
|
|
345
|
+
// click that never moved ended the attempt outright.
|
|
346
|
+
test("a grab released without moving reports nothing", () => {
|
|
376
347
|
render(props());
|
|
377
348
|
mouseDown(20, 100);
|
|
378
349
|
mouseUp();
|
|
379
|
-
expect(onComplete).
|
|
350
|
+
expect(onComplete).not.toHaveBeenCalled();
|
|
380
351
|
});
|
|
381
352
|
|
|
382
353
|
test("letting go without having grabbed anything reports nothing", () => {
|
|
@@ -412,7 +383,9 @@ describe("dragging with a mouse", () => {
|
|
|
412
383
|
mouseMove(200, 80);
|
|
413
384
|
mouseUp();
|
|
414
385
|
const events = onComplete.mock.calls[1]?.[2];
|
|
415
|
-
expect(events?.map((event) => [event.x, event.y])).toEqual([
|
|
386
|
+
expect(events?.map((event: PuzzleEvent) => [event.x, event.y])).toEqual([
|
|
387
|
+
[200, 80],
|
|
388
|
+
]);
|
|
416
389
|
});
|
|
417
390
|
|
|
418
391
|
test("the trail is timestamped in order", () => {
|
|
@@ -465,6 +438,19 @@ describe("dragging with a finger", () => {
|
|
|
465
438
|
expect(onComplete).not.toHaveBeenCalled();
|
|
466
439
|
});
|
|
467
440
|
|
|
441
|
+
// The panel opens centred, under the finger that just pressed the checkbox,
|
|
442
|
+
// so tapping the piece instead of dragging it is the easy mistake to make on
|
|
443
|
+
// a phone. It used to submit the untouched origin, which fails, and the
|
|
444
|
+
// widget then reset — indistinguishable to the user from the challenge
|
|
445
|
+
// never having worked.
|
|
446
|
+
test("tapping the piece without dragging reports nothing", () => {
|
|
447
|
+
render(props());
|
|
448
|
+
touchStart([{ clientX: 20, clientY: 100 }]);
|
|
449
|
+
touchEnd();
|
|
450
|
+
expect(onComplete).not.toHaveBeenCalled();
|
|
451
|
+
expect(piecePosition()).toEqual({ x: 20, y: 100 });
|
|
452
|
+
});
|
|
453
|
+
|
|
468
454
|
test("a touchmove carrying no touches is ignored mid-drag", () => {
|
|
469
455
|
render(props());
|
|
470
456
|
touchStart([{ clientX: 20, clientY: 100 }]);
|
|
@@ -586,11 +572,9 @@ describe("driving it from the keyboard", () => {
|
|
|
586
572
|
|
|
587
573
|
test("tab cannot leave the dialog for the page behind it", () => {
|
|
588
574
|
render(props());
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
);
|
|
593
|
-
});
|
|
575
|
+
document.dispatchEvent(
|
|
576
|
+
new KeyboardEvent("keydown", { key: "Tab", bubbles: true }),
|
|
577
|
+
);
|
|
594
578
|
expect(document.activeElement).toBe(piece());
|
|
595
579
|
});
|
|
596
580
|
|
|
@@ -602,15 +586,10 @@ describe("driving it from the keyboard", () => {
|
|
|
602
586
|
render(props());
|
|
603
587
|
expect(document.activeElement).toBe(piece());
|
|
604
588
|
|
|
605
|
-
|
|
606
|
-
root.unmount();
|
|
607
|
-
});
|
|
589
|
+
destroy();
|
|
608
590
|
expect(document.activeElement).toBe(opener);
|
|
609
591
|
|
|
610
592
|
opener.remove();
|
|
611
|
-
act(() => {
|
|
612
|
-
root = createRoot(container);
|
|
613
|
-
});
|
|
614
593
|
});
|
|
615
594
|
});
|
|
616
595
|
|
|
@@ -647,9 +626,7 @@ describe("what it says to a screen reader", () => {
|
|
|
647
626
|
keyDown("ArrowRight");
|
|
648
627
|
keyDown("ArrowRight");
|
|
649
628
|
expect(liveRegion().textContent).toBe("7 percent across, 50 percent down");
|
|
650
|
-
|
|
651
|
-
vi.advanceTimersByTime(500);
|
|
652
|
-
});
|
|
629
|
+
vi.advanceTimersByTime(500);
|
|
653
630
|
expect(liveRegion().textContent).toBe("13 percent across, 50 percent down");
|
|
654
631
|
});
|
|
655
632
|
|
|
@@ -677,14 +654,9 @@ describe("after it goes away", () => {
|
|
|
677
654
|
test("its document listeners go with it", () => {
|
|
678
655
|
render(props());
|
|
679
656
|
mouseDown(20, 100);
|
|
680
|
-
|
|
681
|
-
root.unmount();
|
|
682
|
-
});
|
|
657
|
+
destroy();
|
|
683
658
|
mouseMove(150, 90);
|
|
684
659
|
mouseUp();
|
|
685
660
|
expect(onComplete).not.toHaveBeenCalled();
|
|
686
|
-
act(() => {
|
|
687
|
-
root = createRoot(container);
|
|
688
|
-
});
|
|
689
661
|
});
|
|
690
662
|
});
|
package/tsconfig.cjs.json
CHANGED