@prosopo/procaptcha-puzzle 2.13.3 → 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 +41 -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
@@ -1,22 +1,27 @@
1
- import { act, createElement } from "react";
2
- import { createRoot } from "react-dom/client";
3
1
  import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
4
2
  import * as entrypoint from "../index.js";
3
+ import { mount, settle } from "./domHarness.js";
5
4
  import { config } from "./managerHarness.js";
6
5
  const mocks = vi.hoisted(() => ({
7
6
  received: [],
7
+ destroyed: { count: 0 },
8
8
  }));
9
- vi.mock("../components/ProcaptchaWidget.js", async () => {
10
- const { createElement: create } = await import("react");
11
- return {
12
- default: (props) => {
13
- mocks.received.push(props);
14
- return create("div", { "data-testid": "widget" });
15
- },
16
- };
17
- });
18
- let container;
19
- let root;
9
+ vi.mock("../components/procaptchaWidget.js", () => ({
10
+ mountProcaptchaPuzzleWidget: (container, props) => {
11
+ mocks.received.push(props);
12
+ const element = document.createElement("div");
13
+ element.setAttribute("data-testid", "widget");
14
+ container.appendChild(element);
15
+ return {
16
+ destroy: () => {
17
+ element.remove();
18
+ mocks.destroyed.count += 1;
19
+ },
20
+ };
21
+ },
22
+ }));
23
+ let mounted;
24
+ let widget;
20
25
  const props = (overrides = {}) => ({
21
26
  config: config(),
22
27
  callbacks: {},
@@ -24,28 +29,24 @@ const props = (overrides = {}) => ({
24
29
  ...overrides,
25
30
  });
26
31
  const render = async (widgetProps) => {
27
- await act(async () => {
28
- root.render(createElement(entrypoint.ProcaptchaPuzzle, widgetProps));
29
- });
32
+ widget = entrypoint.mountProcaptchaPuzzle(mounted.container, widgetProps);
33
+ await settle();
30
34
  };
31
35
  beforeEach(() => {
32
36
  mocks.received.length = 0;
33
- container = document.createElement("div");
34
- document.body.appendChild(container);
35
- act(() => {
36
- root = createRoot(container);
37
- });
37
+ mocks.destroyed.count = 0;
38
+ widget = undefined;
39
+ mounted = mount();
38
40
  });
39
41
  afterEach(() => {
40
- act(() => {
41
- root.unmount();
42
- });
43
- container.remove();
42
+ widget?.destroy();
43
+ widget = undefined;
44
+ mounted.unmount();
44
45
  });
45
- describe("ProcaptchaPuzzle", () => {
46
+ describe("mountProcaptchaPuzzle", () => {
46
47
  test("renders the widget once it has loaded", async () => {
47
48
  await render(props());
48
- expect(container.querySelector('[data-testid="widget"]')).not.toBeNull();
49
+ expect(mounted.container.querySelector('[data-testid="widget"]')).not.toBeNull();
49
50
  });
50
51
  test("passes on the props the widget needs but never names", async () => {
51
52
  const onSessionInvalidated = vi.fn();
@@ -66,16 +67,26 @@ describe("ProcaptchaPuzzle", () => {
66
67
  expect(mocks.received[0]?.callbacks).toBe(callbacks);
67
68
  expect(mocks.received[0]?.config).toEqual(config());
68
69
  });
69
- test("mounts one widget per render, not one per prop change", async () => {
70
+ test("tears the loaded widget down", async () => {
70
71
  await render(props());
71
- await render(props({ autoStart: true }));
72
- expect(container.querySelectorAll('[data-testid="widget"]')).toHaveLength(1);
72
+ widget?.destroy();
73
+ widget = undefined;
74
+ expect(mocks.destroyed.count).toBe(1);
75
+ expect(mounted.container.querySelector('[data-testid="widget"]')).toBeNull();
76
+ });
77
+ test("cancels a mount that is still loading", async () => {
78
+ widget = entrypoint.mountProcaptchaPuzzle(mounted.container, props());
79
+ widget.destroy();
80
+ widget = undefined;
81
+ await settle();
82
+ expect(mocks.received).toHaveLength(0);
73
83
  });
74
84
  });
75
85
  describe("the package entrypoint", () => {
76
- test("exports the wrapper a consumer mounts", () => {
77
- expect(typeof entrypoint.ProcaptchaPuzzle).toBe("function");
78
- expect(Object.keys(entrypoint)).toEqual(["ProcaptchaPuzzle"]);
86
+ test("exports the mount functions a consumer needs", () => {
87
+ expect(typeof entrypoint.mountProcaptchaPuzzle).toBe("function");
88
+ expect(typeof entrypoint.loadProcaptchaPuzzle).toBe("function");
89
+ expect(typeof entrypoint.mountProcaptchaPuzzleWidget).toBe("function");
79
90
  });
80
91
  });
81
92
  //# sourceMappingURL=procaptchaPuzzle.unit.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"procaptchaPuzzle.unit.test.js","sourceRoot":"","sources":["../../src/tests/procaptchaPuzzle.unit.test.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAqB,GAAG,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC9D,OAAO,EAAa,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC3E,OAAO,KAAK,UAAU,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAQ7C,MAAM,KAAK,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAC/B,QAAQ,EAAE,EAAuB;CACjC,CAAC,CAAC,CAAC;AAEJ,EAAE,CAAC,IAAI,CAAC,mCAAmC,EAAE,KAAK,IAAI,EAAE;IACvD,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,CAAC;IACxD,OAAO;QACN,OAAO,EAAE,CAAC,KAAsB,EAAE,EAAE;YACnC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC3B,OAAO,MAAM,CAAC,KAAK,EAAE,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,CAAC;QACnD,CAAC;KACD,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,IAAI,SAAyB,CAAC;AAC9B,IAAI,IAAU,CAAC;AAEf,MAAM,KAAK,GAAG,CAAC,YAAsC,EAAE,EAAmB,EAAE,CAAC,CAAC;IAC7E,MAAM,EAAE,MAAM,EAAE;IAChB,SAAS,EAAE,EAAE;IACb,IAAI,EAAE,SAA6B;IACnC,GAAG,SAAS;CACZ,CAAC,CAAC;AAEH,MAAM,MAAM,GAAG,KAAK,EAAE,WAA4B,EAAiB,EAAE;IACpE,MAAM,GAAG,CAAC,KAAK,IAAI,EAAE;QACpB,IAAI,CAAC,MAAM,CACV,aAAa,CAAC,UAAU,CAAC,gBAAgB,EAAE,WAAW,CAAiB,CACvE,CAAC;IACH,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC;AAEF,UAAU,CAAC,GAAG,EAAE;IACf,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;IAC1B,SAAS,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IAC1C,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;IACrC,GAAG,CAAC,GAAG,EAAE;QACR,IAAI,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,SAAS,CAAC,GAAG,EAAE;IACd,GAAG,CAAC,GAAG,EAAE;QACR,IAAI,CAAC,OAAO,EAAE,CAAC;IAChB,CAAC,CAAC,CAAC;IACH,SAAS,CAAC,MAAM,EAAE,CAAC;AACpB,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IACjC,IAAI,CAAC,uCAAuC,EAAE,KAAK,IAAI,EAAE;QACxD,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;QACtB,MAAM,CAAC,SAAS,CAAC,aAAa,CAAC,wBAAwB,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;IAC1E,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;QACvE,MAAM,oBAAoB,GAAG,EAAE,CAAC,EAAE,EAAoC,CAAC;QACvE,MAAM,MAAM,CACX,KAAK,CAAC;YACL,SAAS,EAAE,IAAI;YACf,WAAW,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;YAC3B,oBAAoB;SACpB,CAAC,CACF,CAAC;QACF,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC;YACvC,SAAS,EAAE,IAAI;YACf,WAAW,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;YAC3B,oBAAoB;SACpB,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,uCAAuC,EAAE,KAAK,IAAI,EAAE;QACxD,MAAM,SAAS,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC,EAAE,EAA2B,EAAE,CAAC;QAChE,MAAM,MAAM,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC;QACnC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACrD,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,uDAAuD,EAAE,KAAK,IAAI,EAAE;QACxE,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;QACtB,MAAM,MAAM,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QACzC,MAAM,CAAC,SAAS,CAAC,gBAAgB,CAAC,wBAAwB,CAAC,CAAC,CAAC,YAAY,CACxE,CAAC,CACD,CAAC;IACH,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;IACvC,IAAI,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAIlD,MAAM,CAAC,OAAO,UAAU,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC5D,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"procaptchaPuzzle.unit.test.js","sourceRoot":"","sources":["../../src/tests/procaptchaPuzzle.unit.test.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE3E,OAAO,KAAK,UAAU,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAgB,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAQ7C,MAAM,KAAK,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAC/B,QAAQ,EAAE,EAAuB;IACjC,SAAS,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE;CACvB,CAAC,CAAC,CAAC;AAEJ,EAAE,CAAC,IAAI,CAAC,mCAAmC,EAAE,GAAG,EAAE,CAAC,CAAC;IACnD,2BAA2B,EAAE,CAC5B,SAAsB,EACtB,KAAsB,EACG,EAAE;QAC3B,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3B,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC9C,OAAO,CAAC,YAAY,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;QAC9C,SAAS,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAC/B,OAAO;YACN,OAAO,EAAE,GAAG,EAAE;gBACb,OAAO,CAAC,MAAM,EAAE,CAAC;gBACjB,KAAK,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC,CAAC;YAC5B,CAAC;SACD,CAAC;IACH,CAAC;CACD,CAAC,CAAC,CAAC;AAEJ,IAAI,OAAgB,CAAC;AACrB,IAAI,MAA0C,CAAC;AAE/C,MAAM,KAAK,GAAG,CAAC,YAAsC,EAAE,EAAmB,EAAE,CAAC,CAAC;IAC7E,MAAM,EAAE,MAAM,EAAE;IAChB,SAAS,EAAE,EAAE;IACb,IAAI,EAAE,SAA6B;IACnC,GAAG,SAAS;CACZ,CAAC,CAAC;AAEH,MAAM,MAAM,GAAG,KAAK,EAAE,WAA4B,EAAiB,EAAE;IACpE,MAAM,GAAG,UAAU,CAAC,qBAAqB,CAAC,OAAO,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;IAC1E,MAAM,MAAM,EAAE,CAAC;AAChB,CAAC,CAAC;AAEF,UAAU,CAAC,GAAG,EAAE;IACf,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;IAC1B,KAAK,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,CAAC;IAC1B,MAAM,GAAG,SAAS,CAAC;IACnB,OAAO,GAAG,KAAK,EAAE,CAAC;AACnB,CAAC,CAAC,CAAC;AAEH,SAAS,CAAC,GAAG,EAAE;IACd,MAAM,EAAE,OAAO,EAAE,CAAC;IAClB,MAAM,GAAG,SAAS,CAAC;IACnB,OAAO,CAAC,OAAO,EAAE,CAAC;AACnB,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACtC,IAAI,CAAC,uCAAuC,EAAE,KAAK,IAAI,EAAE;QACxD,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;QACtB,MAAM,CACL,OAAO,CAAC,SAAS,CAAC,aAAa,CAAC,wBAAwB,CAAC,CACzD,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;QACvE,MAAM,oBAAoB,GAAG,EAAE,CAAC,EAAE,EAAoC,CAAC;QACvE,MAAM,MAAM,CACX,KAAK,CAAC;YACL,SAAS,EAAE,IAAI;YACf,WAAW,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;YAC3B,oBAAoB;SACpB,CAAC,CACF,CAAC;QACF,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC;YACvC,SAAS,EAAE,IAAI;YACf,WAAW,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;YAC3B,oBAAoB;SACpB,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,uCAAuC,EAAE,KAAK,IAAI,EAAE;QACxD,MAAM,SAAS,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC,EAAE,EAA2B,EAAE,CAAC;QAChE,MAAM,MAAM,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC;QACnC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACrD,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,8BAA8B,EAAE,KAAK,IAAI,EAAE;QAC/C,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;QACtB,MAAM,EAAE,OAAO,EAAE,CAAC;QAClB,MAAM,GAAG,SAAS,CAAC;QACnB,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACtC,MAAM,CACL,OAAO,CAAC,SAAS,CAAC,aAAa,CAAC,wBAAwB,CAAC,CACzD,CAAC,QAAQ,EAAE,CAAC;IACd,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,uCAAuC,EAAE,KAAK,IAAI,EAAE;QAGxD,MAAM,GAAG,UAAU,CAAC,qBAAqB,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;QACtE,MAAM,CAAC,OAAO,EAAE,CAAC;QACjB,MAAM,GAAG,SAAS,CAAC;QACnB,MAAM,MAAM,EAAE,CAAC;QACf,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;IACvC,IAAI,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACzD,MAAM,CAAC,OAAO,UAAU,CAAC,qBAAqB,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACjE,MAAM,CAAC,OAAO,UAAU,CAAC,oBAAoB,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAChE,MAAM,CAAC,OAAO,UAAU,CAAC,2BAA2B,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC"}
@@ -1,8 +1,7 @@
1
1
  import { ModeEnum, } from "@prosopo/types";
2
- import { act, createElement } from "react";
3
- import { createRoot } from "react-dom/client";
4
2
  import { afterEach, beforeEach, describe, expect, test, vi, } from "vitest";
5
- import Procaptcha from "../components/ProcaptchaWidget.js";
3
+ import { mountProcaptchaPuzzleWidget, } from "../components/procaptchaWidget.js";
4
+ import { fire, mount, settle } from "./domHarness.js";
6
5
  import { challengeResponse, config, frictionless } from "./managerHarness.js";
7
6
  const mocks = vi.hoisted(() => {
8
7
  const start = vi.fn();
@@ -10,9 +9,9 @@ const mocks = vi.hoisted(() => {
10
9
  const resetState = vi.fn();
11
10
  const constructions = [];
12
11
  const loadI18next = vi.fn();
13
- const checkboxProps = { current: undefined };
14
- const canvasProps = { current: undefined };
15
- const honeypotQuestions = [];
12
+ const canvasProps = {
13
+ current: undefined,
14
+ };
16
15
  const translationsReady = { current: true };
17
16
  return {
18
17
  translationsReady,
@@ -21,9 +20,7 @@ const mocks = vi.hoisted(() => {
21
20
  resetState,
22
21
  constructions,
23
22
  loadI18next,
24
- checkboxProps,
25
23
  canvasProps,
26
- honeypotQuestions,
27
24
  };
28
25
  });
29
26
  vi.mock("../services/Manager.js", () => ({
@@ -36,47 +33,38 @@ vi.mock("../services/Manager.js", () => ({
36
33
  };
37
34
  },
38
35
  }));
39
- vi.mock("../components/PuzzleCanvas.js", async () => {
40
- const { createElement: create } = await import("react");
41
- const PuzzleCanvas = (canvasProps) => {
36
+ vi.mock("../components/puzzleCanvas.js", () => ({
37
+ mountPuzzleCanvas: (canvasProps) => {
42
38
  mocks.canvasProps.current = canvasProps;
43
- return create("div", { "data-cy": "canvas-stub" });
44
- };
45
- return { PuzzleCanvas };
46
- });
47
- vi.mock("@prosopo/procaptcha-common", async (importOriginal) => {
48
- const actual = await importOriginal();
49
- const { createElement: create, forwardRef } = await import("react");
50
- const Checkbox = (checkboxProps) => {
51
- mocks.checkboxProps.current = checkboxProps;
52
- return create("input", {
53
- type: "checkbox",
54
- readOnly: true,
55
- checked: checkboxProps.checked,
56
- "aria-label": checkboxProps.labelText,
57
- "data-error": checkboxProps.error,
58
- "data-loading": String(checkboxProps.loading),
59
- });
60
- };
61
- const Honeypot = forwardRef(({ encodedQuestion }, ref) => {
62
- mocks.honeypotQuestions.push(encodedQuestion);
63
- return create("input", { type: "text", ref, name: "honeypot" });
64
- });
65
- return { ...actual, Checkbox, Honeypot };
66
- });
39
+ const element = document.createElement("div");
40
+ element.setAttribute("data-cy", "canvas-stub");
41
+ document.body.appendChild(element);
42
+ return {
43
+ update: (next) => {
44
+ mocks.canvasProps.current = next;
45
+ },
46
+ destroy: () => {
47
+ element.remove();
48
+ mocks.canvasProps.current = undefined;
49
+ },
50
+ };
51
+ },
52
+ }));
67
53
  vi.mock("@prosopo/locale", async (importOriginal) => {
68
54
  const actual = await importOriginal();
69
55
  return {
70
56
  ...actual,
71
57
  loadI18next: mocks.loadI18next,
72
- useTranslation: () => ({
58
+ createTranslator: () => ({
73
59
  t: (key) => key,
74
- ready: mocks.translationsReady.current,
60
+ isReady: () => mocks.translationsReady.current,
61
+ subscribe: () => () => undefined,
62
+ i18n: undefined,
75
63
  }),
76
64
  };
77
65
  });
78
- let container;
79
- let root;
66
+ let mounted;
67
+ let widget;
80
68
  const i18nStub = (language, changeLanguage) => ({ language, changeLanguage });
81
69
  const props = (overrides = {}) => ({
82
70
  config: config(),
@@ -85,69 +73,53 @@ const props = (overrides = {}) => ({
85
73
  ...overrides,
86
74
  });
87
75
  const render = (widgetProps) => {
88
- act(() => {
89
- root.render(createElement(Procaptcha, widgetProps));
90
- });
76
+ widget = mountProcaptchaPuzzleWidget(mounted.container, widgetProps);
77
+ };
78
+ const destroy = () => {
79
+ widget?.destroy();
80
+ widget = undefined;
91
81
  };
92
82
  beforeEach(() => {
93
83
  vi.clearAllMocks();
94
84
  mocks.constructions.length = 0;
95
- mocks.honeypotQuestions.length = 0;
96
85
  mocks.translationsReady.current = true;
97
- mocks.checkboxProps.current = undefined;
98
86
  mocks.canvasProps.current = undefined;
99
87
  mocks.start.mockResolvedValue(challengeResponse());
100
88
  mocks.submitSolution.mockResolvedValue(true);
101
89
  mocks.loadI18next.mockResolvedValue(undefined);
102
- container = document.createElement("div");
103
- document.body.appendChild(container);
104
- act(() => {
105
- root = createRoot(container);
106
- });
90
+ widget = undefined;
91
+ mounted = mount();
107
92
  });
108
93
  afterEach(() => {
109
- act(() => {
110
- root.unmount();
111
- });
112
- container.remove();
94
+ destroy();
95
+ mounted.unmount();
113
96
  vi.useRealTimers();
114
97
  vi.restoreAllMocks();
115
98
  });
116
99
  const checkbox = () => {
117
- const element = container.querySelector('input[type="checkbox"]');
100
+ const element = mounted.container.querySelector('input[type="checkbox"]');
118
101
  if (!element)
119
102
  throw new Error("expected a checkbox to be rendered");
120
103
  return element;
121
104
  };
122
- const canvas = () => container.querySelector('[data-cy="canvas-stub"]');
105
+ const canvas = () => document.body.querySelector('[data-cy="canvas-stub"]');
106
+ const spinner = () => mounted.container.querySelector('[role="status"]');
123
107
  const honeypotInput = () => {
124
- const element = container.querySelector('input[name="honeypot"]');
108
+ const element = document.querySelector('input[name="email_confirm"]');
125
109
  if (!element)
126
110
  throw new Error("expected a honeypot to be rendered");
127
111
  return element;
128
112
  };
129
113
  const click = async (options = {}) => {
130
- const nativeEvent = {
131
- isTrusted: options.trusted ?? true,
132
- clientX: options.clientX ?? 0,
133
- clientY: options.clientY ?? 0,
134
- ...(options.touches ? { touches: options.touches } : {}),
135
- };
136
- await act(async () => {
137
- await mocks.checkboxProps.current?.onChange({ nativeEvent });
138
- });
114
+ fire(checkbox(), "click", options);
115
+ await settle();
139
116
  };
140
117
  const clickWithoutWaiting = () => {
141
- act(() => {
142
- void mocks.checkboxProps.current?.onChange({
143
- nativeEvent: { isTrusted: true, clientX: 0, clientY: 0 },
144
- });
145
- });
118
+ fire(checkbox(), "click");
146
119
  };
147
120
  const complete = async (x = 200, y = 80, events = [{ x: 200, y: 80, t: 5 }]) => {
148
- await act(async () => {
149
- await mocks.canvasProps.current?.onComplete(x, y, events);
150
- });
121
+ await mocks.canvasProps.current?.onComplete(x, y, events);
122
+ await settle();
151
123
  };
152
124
  describe("what the widget renders", () => {
153
125
  test("a visible widget shows a checkbox", () => {
@@ -156,23 +128,23 @@ describe("what the widget renders", () => {
156
128
  });
157
129
  test("an invisible widget shows no checkbox at all", () => {
158
130
  render(props({ config: config({ mode: ModeEnum.invisible }) }));
159
- expect(container.querySelector('input[type="checkbox"]')).toBeNull();
131
+ expect(mounted.container.querySelector('input[type="checkbox"]')).toBeNull();
160
132
  });
161
133
  test("no puzzle is shown until a challenge has been fetched", () => {
162
134
  render(props());
163
135
  expect(canvas()).toBeNull();
164
136
  });
165
137
  test("a session with a honeypot question renders the honeypot", () => {
166
- render(props({ frictionlessState: frictionless({ hp: "question" }) }));
138
+ render(props({ frictionlessState: frictionless({ hp: btoa("question") }) }));
167
139
  expect(honeypotInput()).toBeDefined();
168
- expect(mocks.honeypotQuestions.at(-1)).toBe("question");
140
+ expect(document.body.textContent).toContain("question");
169
141
  });
170
142
  test("a session without one renders no bait at all", () => {
171
143
  render(props({ frictionlessState: frictionless() }));
172
- expect(container.querySelector('input[name="honeypot"]')).toBeNull();
144
+ expect(document.querySelector('input[name="email_confirm"]')).toBeNull();
173
145
  });
174
146
  test("the manager can read the honeypot input the widget rendered", () => {
175
- render(props({ frictionlessState: frictionless({ hp: "question" }) }));
147
+ render(props({ frictionlessState: frictionless({ hp: btoa("question") }) }));
176
148
  honeypotInput().value = "bot@example.com";
177
149
  expect(mocks.constructions[0]?.getHoneypotValue?.()).toBe("bot@example.com");
178
150
  });
@@ -182,25 +154,17 @@ describe("what the widget renders", () => {
182
154
  });
183
155
  test("the checkbox is labelled once the translations are ready", () => {
184
156
  render(props());
185
- expect(mocks.checkboxProps.current?.labelText).toBe("WIDGET.I_AM_HUMAN");
157
+ expect(checkbox().getAttribute("aria-label")).toBe("WIDGET.I_AM_HUMAN");
186
158
  });
187
159
  test("the label is left empty while the translations load", () => {
188
160
  mocks.translationsReady.current = false;
189
161
  render(props());
190
- expect(mocks.checkboxProps.current?.labelText).toBe("");
162
+ expect(checkbox().getAttribute("aria-label")).toBe("");
191
163
  });
192
- test("the widget keeps the manager it started with across re-renders", () => {
193
- render(props());
164
+ test("the widget builds exactly one manager", async () => {
194
165
  render(props());
195
- act(() => {
196
- mocks.constructions[0]?.updateState({
197
- error: {
198
- message: "from the original manager",
199
- key: "API.UNKNOWN_ERROR",
200
- },
201
- });
202
- });
203
- expect(mocks.checkboxProps.current?.error).toBe("from the original manager");
166
+ await click();
167
+ expect(mocks.constructions).toHaveLength(1);
204
168
  });
205
169
  });
206
170
  describe("language", () => {
@@ -243,17 +207,17 @@ describe("starting from the checkbox", () => {
243
207
  showRetry: false,
244
208
  });
245
209
  });
246
- test("an untrusted click starts a solve but carries no coordinates", async () => {
210
+ test("a synthetic click never reaches the manager", async () => {
247
211
  render(props());
248
212
  await click({ trusted: false, clientX: 12, clientY: 34 });
249
- expect(mocks.start).toHaveBeenCalledWith(0, 0);
213
+ expect(mocks.start).not.toHaveBeenCalled();
250
214
  });
251
215
  test("a tap reports the coordinates of the first touch", async () => {
252
216
  render(props());
253
217
  await click({ touches: [{ clientX: 7, clientY: 9 }] });
254
218
  expect(mocks.start).toHaveBeenCalledWith(7, 9);
255
219
  });
256
- test("a touch event with no touches falls back to the origin", async () => {
220
+ test("a touch event with no touches falls back to the pointer", async () => {
257
221
  render(props());
258
222
  await click({ touches: [] });
259
223
  expect(mocks.start).toHaveBeenCalledWith(0, 0);
@@ -268,9 +232,8 @@ describe("starting from the checkbox", () => {
268
232
  clickWithoutWaiting();
269
233
  clickWithoutWaiting();
270
234
  expect(mocks.start).toHaveBeenCalledTimes(1);
271
- await act(async () => {
272
- release(challengeResponse());
273
- });
235
+ release(challengeResponse());
236
+ await settle();
274
237
  });
275
238
  test("the checkbox shows a spinner while the solve is loading", async () => {
276
239
  let release = () => {
@@ -280,55 +243,52 @@ describe("starting from the checkbox", () => {
280
243
  }));
281
244
  render(props());
282
245
  clickWithoutWaiting();
283
- expect(mocks.checkboxProps.current?.loading).toBe(true);
284
- await act(async () => {
285
- release(challengeResponse());
286
- });
287
- expect(mocks.checkboxProps.current?.loading).toBe(false);
246
+ await settle();
247
+ expect(spinner()).not.toBeNull();
248
+ release(challengeResponse());
249
+ await settle();
250
+ expect(spinner()).toBeNull();
288
251
  });
289
252
  test("a start that yields no challenge leaves the checkbox in place", async () => {
290
253
  mocks.start.mockResolvedValue(undefined);
291
254
  render(props());
292
255
  await click();
293
256
  expect(canvas()).toBeNull();
294
- expect(mocks.checkboxProps.current?.loading).toBe(false);
257
+ expect(spinner()).toBeNull();
295
258
  });
296
259
  test("a start that throws returns the user to a usable checkbox", async () => {
297
260
  const onError = vi.fn();
298
261
  mocks.start.mockRejectedValue(new Error("provider down"));
299
262
  render(props({ callbacks: { onError } }));
300
263
  await click();
301
- expect(mocks.checkboxProps.current?.loading).toBe(false);
264
+ expect(spinner()).toBeNull();
302
265
  expect(onError).toHaveBeenCalledWith(expect.any(Error));
303
266
  });
304
267
  test("a start that throws without an error callback is still recoverable", async () => {
305
268
  mocks.start.mockRejectedValue(new Error("provider down"));
306
269
  render(props());
307
270
  await click();
308
- expect(mocks.checkboxProps.current?.loading).toBe(false);
271
+ expect(spinner()).toBeNull();
309
272
  });
310
273
  });
311
274
  describe("autoStart", () => {
312
275
  test("fetches a challenge and opens the puzzle without a click", async () => {
313
- await act(async () => {
314
- root.render(createElement(Procaptcha, props({ autoStart: true, startCoords: { x: 5, y: 6 } })));
315
- });
276
+ render(props({ autoStart: true, startCoords: { x: 5, y: 6 } }));
277
+ await settle();
316
278
  expect(mocks.start).toHaveBeenCalledWith(5, 6);
317
279
  expect(canvas()).not.toBeNull();
318
280
  });
319
281
  test("starts at the origin when no coordinates were handed over", async () => {
320
- await act(async () => {
321
- root.render(createElement(Procaptcha, props({ autoStart: true })));
322
- });
282
+ render(props({ autoStart: true }));
283
+ await settle();
323
284
  expect(mocks.start).toHaveBeenCalledWith(0, 0);
324
285
  });
325
286
  test("a failed autoStart leaves the checkbox usable", async () => {
326
287
  mocks.start.mockRejectedValue(new Error("nope"));
327
- await act(async () => {
328
- root.render(createElement(Procaptcha, props({ autoStart: true })));
329
- });
288
+ render(props({ autoStart: true }));
289
+ await settle();
330
290
  expect(canvas()).toBeNull();
331
- expect(mocks.checkboxProps.current?.loading).toBe(false);
291
+ expect(spinner()).toBeNull();
332
292
  });
333
293
  test("no autoStart means no solve until the user acts", () => {
334
294
  render(props());
@@ -350,7 +310,7 @@ describe("finishing the drag", () => {
350
310
  await openPuzzle();
351
311
  await complete();
352
312
  expect(canvas()).toBeNull();
353
- expect(mocks.checkboxProps.current?.loading).toBe(false);
313
+ expect(spinner()).toBeNull();
354
314
  });
355
315
  test("a rejected solution asks for another go on a fresh challenge", async () => {
356
316
  mocks.submitSolution.mockResolvedValue(false);
@@ -411,22 +371,18 @@ describe("finishing the drag", () => {
411
371
  release = resolve;
412
372
  }));
413
373
  await openPuzzle();
414
- act(() => {
415
- void mocks.canvasProps.current?.onComplete(200, 80, []);
416
- });
374
+ void mocks.canvasProps.current?.onComplete(200, 80, []);
375
+ await settle();
417
376
  expect(mocks.canvasProps.current?.submitting).toBe(true);
418
- expect(mocks.checkboxProps.current?.loading).toBe(true);
419
- await act(async () => {
420
- release(true);
421
- });
377
+ expect(spinner()).not.toBeNull();
378
+ release(true);
379
+ await settle();
422
380
  });
423
381
  });
424
382
  describe("invisible mode", () => {
425
383
  const execute = async () => {
426
- await act(async () => {
427
- document.dispatchEvent(new Event("procaptcha:execute"));
428
- await Promise.resolve();
429
- });
384
+ document.dispatchEvent(new Event("procaptcha:execute"));
385
+ await settle();
430
386
  };
431
387
  test("an execute event fetches a challenge and opens the puzzle", async () => {
432
388
  render(props({ config: config({ mode: ModeEnum.invisible }) }));
@@ -442,10 +398,8 @@ describe("invisible mode", () => {
442
398
  test("a targeted execute on the container runs a visible widget", async () => {
443
399
  const target = document.createElement("div");
444
400
  render(props({ container: target }));
445
- await act(async () => {
446
- target.dispatchEvent(new Event("procaptcha:execute"));
447
- await Promise.resolve();
448
- });
401
+ target.dispatchEvent(new Event("procaptcha:execute"));
402
+ await settle();
449
403
  expect(mocks.start).toHaveBeenCalledTimes(1);
450
404
  expect(canvas()).not.toBeNull();
451
405
  });
@@ -455,10 +409,8 @@ describe("invisible mode", () => {
455
409
  config: config({ mode: ModeEnum.invisible }),
456
410
  container: target,
457
411
  }));
458
- await act(async () => {
459
- target.dispatchEvent(new Event("procaptcha:execute"));
460
- await Promise.resolve();
461
- });
412
+ target.dispatchEvent(new Event("procaptcha:execute"));
413
+ await settle();
462
414
  expect(mocks.start).toHaveBeenCalledTimes(1);
463
415
  });
464
416
  test("an execute that yields no challenge shows no puzzle", async () => {
@@ -487,85 +439,66 @@ describe("invisible mode", () => {
487
439
  await execute();
488
440
  expect(onError).toHaveBeenCalledWith(expect.any(Error));
489
441
  });
490
- test("the listener is dropped when the widget unmounts", async () => {
442
+ test("the listener is dropped when the widget is destroyed", async () => {
491
443
  render(props({ config: config({ mode: ModeEnum.invisible }) }));
492
- act(() => {
493
- root.unmount();
494
- });
495
- act(() => {
496
- root = createRoot(container);
497
- });
444
+ destroy();
498
445
  await execute();
499
446
  expect(mocks.start).not.toHaveBeenCalled();
500
447
  });
501
448
  });
502
449
  describe("an invalidated session", () => {
503
- const invalidate = (key = "CAPTCHA.NO_SESSION_FOUND", message = "session gone") => {
504
- act(() => {
505
- mocks.constructions[0]?.updateState({ error: { message, key } });
506
- });
450
+ const invalidate = async (key = "CAPTCHA.NO_SESSION_FOUND", message = "session gone") => {
451
+ mocks.constructions[0]?.updateState({ error: { message, key } });
452
+ await settle();
507
453
  };
508
454
  test("the error is shown on the checkbox and the puzzle is torn down", async () => {
509
455
  render(props());
510
456
  await click();
511
- invalidate("API.UNKNOWN_ERROR", "something broke");
512
- expect(mocks.checkboxProps.current?.error).toBe("something broke");
457
+ await invalidate("API.UNKNOWN_ERROR", "something broke");
458
+ expect(mounted.container.textContent).toContain("something broke");
513
459
  expect(canvas()).toBeNull();
514
- expect(mocks.checkboxProps.current?.loading).toBe(false);
460
+ expect(spinner()).toBeNull();
515
461
  });
516
462
  test("the host is told to re-mint, with the coordinates of the original click", async () => {
517
463
  const onSessionInvalidated = vi.fn();
518
464
  render(props({ onSessionInvalidated }));
519
465
  await click({ clientX: 12, clientY: 34 });
520
- invalidate();
466
+ await invalidate();
521
467
  expect(onSessionInvalidated).toHaveBeenCalledWith(12, 34);
522
468
  });
523
469
  test("the host is told only once, however many errors arrive", async () => {
524
470
  const onSessionInvalidated = vi.fn();
525
471
  render(props({ onSessionInvalidated }));
526
472
  await click();
527
- invalidate("CAPTCHA.NO_SESSION_FOUND", "gone");
528
- invalidate("CAPTCHA.NO_SESSION_FOUND", "gone again");
473
+ await invalidate("CAPTCHA.NO_SESSION_FOUND", "gone");
474
+ await invalidate("CAPTCHA.NO_SESSION_FOUND", "gone again");
529
475
  expect(onSessionInvalidated).toHaveBeenCalledTimes(1);
530
476
  });
531
477
  test("with no host handler the frictionless session restarts instead", async () => {
532
- vi.useFakeTimers();
533
478
  const restart = vi.fn();
534
479
  render(props({ frictionlessState: frictionless({ restart }) }));
535
- invalidate();
480
+ await invalidate();
536
481
  expect(restart).not.toHaveBeenCalled();
537
- await act(async () => {
538
- await vi.advanceTimersByTimeAsync(100);
539
- });
482
+ await new Promise((resolve) => setTimeout(resolve, 150));
540
483
  expect(restart).toHaveBeenCalledTimes(1);
541
- vi.useRealTimers();
542
484
  });
543
- test("a restart pending at unmount is cancelled", async () => {
544
- vi.useFakeTimers();
485
+ test("a restart pending at destroy is cancelled", async () => {
545
486
  const restart = vi.fn();
546
487
  render(props({ frictionlessState: frictionless({ restart }) }));
547
- invalidate();
548
- act(() => {
549
- root.unmount();
550
- });
551
- await act(async () => {
552
- await vi.advanceTimersByTimeAsync(200);
553
- });
488
+ await invalidate();
489
+ destroy();
490
+ await new Promise((resolve) => setTimeout(resolve, 200));
554
491
  expect(restart).not.toHaveBeenCalled();
555
- act(() => {
556
- root = createRoot(container);
557
- });
558
- vi.useRealTimers();
559
492
  });
560
493
  test("a lost session with nothing to recover it is simply surfaced", async () => {
561
494
  render(props());
562
- invalidate();
563
- expect(mocks.checkboxProps.current?.error).toBe("session gone");
495
+ await invalidate();
496
+ expect(mounted.container.textContent).toContain("session gone");
564
497
  });
565
- test("no coordinates are reported when the session was never clicked into", () => {
498
+ test("no coordinates are reported when the session was never clicked into", async () => {
566
499
  const onSessionInvalidated = vi.fn();
567
500
  render(props({ onSessionInvalidated }));
568
- invalidate();
501
+ await invalidate();
569
502
  expect(onSessionInvalidated).toHaveBeenCalledWith(undefined, undefined);
570
503
  });
571
504
  });