@prosopo/procaptcha-puzzle 2.13.2 → 2.14.0

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.
Files changed (72) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +6 -6
  2. package/.turbo/turbo-build$colon$tsc.log +19 -19
  3. package/.turbo/turbo-build.log +7 -7
  4. package/CHANGELOG.md +50 -0
  5. package/dist/cjs/components/procaptchaPuzzle.cjs +22 -0
  6. package/dist/cjs/components/procaptchaWidget.cjs +225 -0
  7. package/dist/cjs/components/puzzleCanvas.cjs +450 -0
  8. package/dist/cjs/index.cjs +7 -2
  9. package/dist/components/procaptchaPuzzle.d.ts +7 -0
  10. package/dist/components/procaptchaPuzzle.d.ts.map +1 -0
  11. package/dist/components/procaptchaPuzzle.js +21 -0
  12. package/dist/components/procaptchaPuzzle.js.map +1 -0
  13. package/dist/components/procaptchaWidget.d.ts +7 -0
  14. package/dist/components/procaptchaWidget.d.ts.map +1 -0
  15. package/dist/components/procaptchaWidget.js +225 -0
  16. package/dist/components/procaptchaWidget.js.map +1 -0
  17. package/dist/components/{PuzzleCanvas.d.ts → puzzleCanvas.d.ts} +6 -4
  18. package/dist/components/puzzleCanvas.d.ts.map +1 -0
  19. package/dist/components/puzzleCanvas.js +450 -0
  20. package/dist/components/puzzleCanvas.js.map +1 -0
  21. package/dist/index.d.ts +3 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +4 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/tests/domHarness.d.ts +20 -0
  26. package/dist/tests/domHarness.d.ts.map +1 -0
  27. package/dist/tests/domHarness.js +56 -0
  28. package/dist/tests/domHarness.js.map +1 -0
  29. package/dist/tests/procaptchaPuzzle.test-d.js +13 -10
  30. package/dist/tests/procaptchaPuzzle.test-d.js.map +1 -1
  31. package/dist/tests/procaptchaPuzzle.unit.test.js +44 -33
  32. package/dist/tests/procaptchaPuzzle.unit.test.js.map +1 -1
  33. package/dist/tests/procaptchaWidget.unit.test.js +108 -175
  34. package/dist/tests/procaptchaWidget.unit.test.js.map +1 -1
  35. package/dist/tests/puzzleCanvas.unit.test.js +47 -80
  36. package/dist/tests/puzzleCanvas.unit.test.js.map +1 -1
  37. package/package.json +8 -12
  38. package/src/components/procaptchaPuzzle.ts +54 -0
  39. package/src/components/procaptchaWidget.ts +392 -0
  40. package/src/components/puzzleCanvas.ts +653 -0
  41. package/src/index.ts +3 -2
  42. package/src/tests/domHarness.ts +122 -0
  43. package/src/tests/procaptchaPuzzle.test-d.ts +33 -17
  44. package/src/tests/procaptchaPuzzle.unit.test.ts +59 -44
  45. package/src/tests/procaptchaWidget.unit.test.ts +145 -258
  46. package/src/tests/puzzleCanvas.unit.test.ts +76 -120
  47. package/tsconfig.cjs.json +1 -2
  48. package/tsconfig.json +1 -2
  49. package/tsconfig.tsbuildinfo +1 -1
  50. package/vite.test.config.ts +3 -4
  51. package/dist/cjs/components/ProcaptchaPuzzle.cjs +0 -7
  52. package/dist/cjs/components/ProcaptchaWidget.cjs +0 -206
  53. package/dist/cjs/components/PuzzleCanvas.cjs +0 -476
  54. package/dist/components/ProcaptchaPuzzle.d.ts +0 -3
  55. package/dist/components/ProcaptchaPuzzle.d.ts.map +0 -1
  56. package/dist/components/ProcaptchaPuzzle.js +0 -7
  57. package/dist/components/ProcaptchaPuzzle.js.map +0 -1
  58. package/dist/components/ProcaptchaWidget.d.ts +0 -4
  59. package/dist/components/ProcaptchaWidget.d.ts.map +0 -1
  60. package/dist/components/ProcaptchaWidget.js +0 -207
  61. package/dist/components/ProcaptchaWidget.js.map +0 -1
  62. package/dist/components/PuzzleCanvas.d.ts.map +0 -1
  63. package/dist/components/PuzzleCanvas.js +0 -476
  64. package/dist/components/PuzzleCanvas.js.map +0 -1
  65. package/dist/tests/setup.d.ts +0 -5
  66. package/dist/tests/setup.d.ts.map +0 -1
  67. package/dist/tests/setup.js +0 -3
  68. package/dist/tests/setup.js.map +0 -1
  69. package/src/components/ProcaptchaPuzzle.tsx +0 -36
  70. package/src/components/ProcaptchaWidget.tsx +0 -320
  71. package/src/components/PuzzleCanvas.tsx +0 -638
  72. 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 { type Theme, lightTheme } from "@prosopo/widget-skeleton";
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 { PuzzleCanvas } from "../components/PuzzleCanvas.js";
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 locale package reaches for an http backend the moment a component
39
- * asks it for a string, which jsdom refuses. The English defaults the canvas
40
- * ships stand in instead, interpolated the way i18next would, so the
41
- * assertions below read as the copy a user is actually given.
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
- vi.mock("@prosopo/locale", async (importOriginal) => {
44
- const actual = await importOriginal<typeof import("@prosopo/locale")>();
45
- const t = (
46
- key: string,
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
- return { ...actual, useTranslation: () => ({ t, ready: true }) };
58
+ },
59
+ isReady: () => true,
60
+ subscribe: () => () => undefined,
61
+ i18n: {} as Translator["i18n"],
55
62
  });
56
63
 
57
- const CONTAINER_WIDTH = 300;
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 = (overrides: Partial<CanvasProps> = {}): CanvasProps => ({
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: 44,
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: CanvasProps): void => {
95
- act(() => {
96
- root.render(createElement(PuzzleCanvas, canvasProps) as ReactElement);
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 portals itself onto the body — it has to escape the query
102
- * container the widget skeleton wraps it in — so it is never inside the render
103
- * container, and everything that reads the rendered output reads the body.
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
- act(() => {
123
- piece().dispatchEvent(
124
- new MouseEvent("mousedown", { bubbles: true, clientX, clientY }),
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
- act(() => {
131
- document.dispatchEvent(
132
- new MouseEvent("mousemove", { bubbles: true, clientX, clientY }),
133
- );
134
- });
126
+ document.dispatchEvent(
127
+ new MouseEvent("mousemove", { bubbles: true, clientX, clientY }),
128
+ );
135
129
  };
136
130
 
137
131
  const mouseUp = (): void => {
138
- act(() => {
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
- act(() => {
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
- act(() => {
164
- document.dispatchEvent(touchEvent("touchmove", touches));
165
- });
153
+ document.dispatchEvent(touchEvent("touchmove", touches));
166
154
  };
167
155
 
168
156
  const touchEnd = (): void => {
169
- act(() => {
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
- act(() => {
176
- piece().dispatchEvent(
177
- new KeyboardEvent("keydown", {
178
- key,
179
- bubbles: true,
180
- cancelable: true,
181
- shiftKey: options.shiftKey ?? false,
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
- container = document.createElement("div");
226
- document.body.appendChild(container);
227
- act(() => {
228
- root = createRoot(container);
229
- });
209
+ canvas = undefined;
230
210
  });
231
211
 
232
212
  afterEach(() => {
233
- act(() => {
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
- act(() => {
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
- act(() => {
306
- root.unmount();
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,7 +334,7 @@ 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
  ]);
@@ -412,7 +380,9 @@ describe("dragging with a mouse", () => {
412
380
  mouseMove(200, 80);
413
381
  mouseUp();
414
382
  const events = onComplete.mock.calls[1]?.[2];
415
- expect(events?.map((event) => [event.x, event.y])).toEqual([[200, 80]]);
383
+ expect(events?.map((event: PuzzleEvent) => [event.x, event.y])).toEqual([
384
+ [200, 80],
385
+ ]);
416
386
  });
417
387
 
418
388
  test("the trail is timestamped in order", () => {
@@ -586,11 +556,9 @@ describe("driving it from the keyboard", () => {
586
556
 
587
557
  test("tab cannot leave the dialog for the page behind it", () => {
588
558
  render(props());
589
- act(() => {
590
- document.dispatchEvent(
591
- new KeyboardEvent("keydown", { key: "Tab", bubbles: true }),
592
- );
593
- });
559
+ document.dispatchEvent(
560
+ new KeyboardEvent("keydown", { key: "Tab", bubbles: true }),
561
+ );
594
562
  expect(document.activeElement).toBe(piece());
595
563
  });
596
564
 
@@ -602,15 +570,10 @@ describe("driving it from the keyboard", () => {
602
570
  render(props());
603
571
  expect(document.activeElement).toBe(piece());
604
572
 
605
- act(() => {
606
- root.unmount();
607
- });
573
+ destroy();
608
574
  expect(document.activeElement).toBe(opener);
609
575
 
610
576
  opener.remove();
611
- act(() => {
612
- root = createRoot(container);
613
- });
614
577
  });
615
578
  });
616
579
 
@@ -647,9 +610,7 @@ describe("what it says to a screen reader", () => {
647
610
  keyDown("ArrowRight");
648
611
  keyDown("ArrowRight");
649
612
  expect(liveRegion().textContent).toBe("7 percent across, 50 percent down");
650
- act(() => {
651
- vi.advanceTimersByTime(500);
652
- });
613
+ vi.advanceTimersByTime(500);
653
614
  expect(liveRegion().textContent).toBe("13 percent across, 50 percent down");
654
615
  });
655
616
 
@@ -677,14 +638,9 @@ describe("after it goes away", () => {
677
638
  test("its document listeners go with it", () => {
678
639
  render(props());
679
640
  mouseDown(20, 100);
680
- act(() => {
681
- root.unmount();
682
- });
641
+ destroy();
683
642
  mouseMove(150, 90);
684
643
  mouseUp();
685
644
  expect(onComplete).not.toHaveBeenCalled();
686
- act(() => {
687
- root = createRoot(container);
688
- });
689
645
  });
690
646
  });
package/tsconfig.cjs.json CHANGED
@@ -3,8 +3,7 @@
3
3
  "compilerOptions": {
4
4
  "rootDir": "./src",
5
5
  "outDir": "./dist/cjs",
6
- "lib": ["es6", "dom"],
7
- "jsxImportSource": "@emotion/react"
6
+ "lib": ["es6", "dom"]
8
7
  },
9
8
  "include": [
10
9
  "./src/**/*.ts",
package/tsconfig.json CHANGED
@@ -3,8 +3,7 @@
3
3
  "compilerOptions": {
4
4
  "rootDir": "./src",
5
5
  "outDir": "./dist",
6
- "lib": ["es6", "dom"],
7
- "jsxImportSource": "@emotion/react"
6
+ "lib": ["es6", "dom"]
8
7
  },
9
8
  "include": [
10
9
  "src",