@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.
- package/.turbo/turbo-build$colon$cjs.log +6 -6
- package/.turbo/turbo-build$colon$tsc.log +19 -19
- package/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +50 -0
- package/dist/cjs/components/procaptchaPuzzle.cjs +22 -0
- package/dist/cjs/components/procaptchaWidget.cjs +225 -0
- package/dist/cjs/components/puzzleCanvas.cjs +450 -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 +450 -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/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 +47 -80
- package/dist/tests/puzzleCanvas.unit.test.js.map +1 -1
- package/package.json +8 -12
- package/src/components/procaptchaPuzzle.ts +54 -0
- package/src/components/procaptchaWidget.ts +392 -0
- package/src/components/puzzleCanvas.ts +653 -0
- package/src/index.ts +3 -2
- package/src/tests/domHarness.ts +122 -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 +76 -120
- 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
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
/**
|
|
16
|
+
* A minimal mount/unmount harness. The suite deliberately drives real DOM
|
|
17
|
+
* events rather than calling handlers directly, because every interactive
|
|
18
|
+
* element in this package drops untrusted events — behaviour that only shows
|
|
19
|
+
* up when a real event carries the flag.
|
|
20
|
+
*/
|
|
21
|
+
export interface Mounted {
|
|
22
|
+
container: HTMLDivElement;
|
|
23
|
+
unmount: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const mount = (): Mounted => {
|
|
27
|
+
const container = document.createElement("div");
|
|
28
|
+
document.body.appendChild(container);
|
|
29
|
+
return {
|
|
30
|
+
container,
|
|
31
|
+
unmount: () => {
|
|
32
|
+
container.remove();
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
interface FireOptions {
|
|
38
|
+
trusted?: boolean;
|
|
39
|
+
clientX?: number;
|
|
40
|
+
clientY?: number;
|
|
41
|
+
touches?: { clientX: number; clientY: number }[];
|
|
42
|
+
key?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* jsdom marks everything it dispatches untrusted, and exposes `isTrusted` as a
|
|
47
|
+
* non-configurable accessor onto its internal implementation object, so the
|
|
48
|
+
* flag has to be set there rather than shadowed on the event. Anything the
|
|
49
|
+
* components filter on untrusted events is untestable otherwise.
|
|
50
|
+
*/
|
|
51
|
+
const setTrusted = (event: Event, trusted: boolean): void => {
|
|
52
|
+
for (const symbol of Object.getOwnPropertySymbols(event)) {
|
|
53
|
+
const impl: unknown = Reflect.get(event, symbol);
|
|
54
|
+
if (impl && typeof impl === "object" && "isTrusted" in impl) {
|
|
55
|
+
// A plain assignment would not survive: dispatchEvent stamps
|
|
56
|
+
// isTrusted back to false on the way in, so the flag is pinned with
|
|
57
|
+
// an accessor that swallows the write.
|
|
58
|
+
Object.defineProperty(impl, "isTrusted", {
|
|
59
|
+
configurable: true,
|
|
60
|
+
get: () => trusted,
|
|
61
|
+
set: () => undefined,
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
throw new Error("could not reach the jsdom event implementation");
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const build = (type: string, options: FireOptions): Event => {
|
|
70
|
+
if (undefined !== options.key) {
|
|
71
|
+
return new KeyboardEvent(type, {
|
|
72
|
+
bubbles: true,
|
|
73
|
+
cancelable: true,
|
|
74
|
+
key: options.key,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return new MouseEvent(type, {
|
|
78
|
+
bubbles: true,
|
|
79
|
+
cancelable: true,
|
|
80
|
+
clientX: options.clientX ?? 0,
|
|
81
|
+
clientY: options.clientY ?? 0,
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Dispatches a real event at the element. Touch points are plain objects:
|
|
87
|
+
* jsdom has no Touch constructor.
|
|
88
|
+
*/
|
|
89
|
+
export const fire = (
|
|
90
|
+
element: Element,
|
|
91
|
+
type: string,
|
|
92
|
+
options: FireOptions = {},
|
|
93
|
+
): void => {
|
|
94
|
+
fireAndReturn(element, type, options);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** Like `fire`, but hands back the event so a test can inspect what the
|
|
98
|
+
* handler did to it — whether it prevented the default form submission, say. */
|
|
99
|
+
export const fireAndReturn = (
|
|
100
|
+
element: Element,
|
|
101
|
+
type: string,
|
|
102
|
+
options: FireOptions = {},
|
|
103
|
+
): Event => {
|
|
104
|
+
const event = build(type, options);
|
|
105
|
+
setTrusted(event, options.trusted ?? true);
|
|
106
|
+
if (options.touches) {
|
|
107
|
+
Object.defineProperty(event, "touches", { value: options.touches });
|
|
108
|
+
}
|
|
109
|
+
element.dispatchEvent(event);
|
|
110
|
+
return event;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Renders coalesce onto the microtask queue, so a state change is not visible
|
|
115
|
+
* in the DOM until it has drained.
|
|
116
|
+
*/
|
|
117
|
+
export const settle = async (): Promise<void> => {
|
|
118
|
+
// A macrotask turn, so a pending dynamic import resolves too — draining the
|
|
119
|
+
// microtask queue alone is not enough for the code-split boundaries.
|
|
120
|
+
await new Promise<void>((resolve: () => void) => setTimeout(resolve, 0));
|
|
121
|
+
await Promise.resolve();
|
|
122
|
+
};
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// See the License for the specific language governing permissions and
|
|
13
13
|
// limitations under the License.
|
|
14
14
|
|
|
15
|
-
import type { Ti18n } from "@prosopo/locale";
|
|
15
|
+
import type { Ti18n, Translator } from "@prosopo/locale";
|
|
16
16
|
import type {
|
|
17
17
|
FrictionlessState,
|
|
18
18
|
GetPuzzleCaptchaResponse,
|
|
@@ -24,11 +24,13 @@ import type {
|
|
|
24
24
|
PuzzleEvent,
|
|
25
25
|
} from "@prosopo/types";
|
|
26
26
|
import { lightTheme } from "@prosopo/widget-skeleton";
|
|
27
|
-
import type { ReactElement } from "react";
|
|
28
27
|
import { assertType, describe, expectTypeOf, test } from "vitest";
|
|
29
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
type PuzzleCanvasProps,
|
|
30
|
+
mountPuzzleCanvas,
|
|
31
|
+
} from "../components/puzzleCanvas.js";
|
|
30
32
|
import type * as entrypoint from "../index.js";
|
|
31
|
-
import {
|
|
33
|
+
import { mountProcaptchaPuzzle } from "../index.js";
|
|
32
34
|
import { Manager } from "../services/Manager.js";
|
|
33
35
|
import {
|
|
34
36
|
challengeResponse,
|
|
@@ -42,17 +44,25 @@ import {
|
|
|
42
44
|
const i18n = (): Ti18n => undefined as unknown as Ti18n;
|
|
43
45
|
|
|
44
46
|
describe("the package entrypoint's types", () => {
|
|
45
|
-
test("
|
|
46
|
-
expectTypeOf(
|
|
47
|
-
[ProcaptchaProps]
|
|
47
|
+
test("mountProcaptchaPuzzle takes a host element and the shared widget props", () => {
|
|
48
|
+
expectTypeOf(mountProcaptchaPuzzle).parameters.toEqualTypeOf<
|
|
49
|
+
[HTMLElement, ProcaptchaProps]
|
|
48
50
|
>();
|
|
49
|
-
expectTypeOf(
|
|
51
|
+
expectTypeOf(mountProcaptchaPuzzle).returns.toExtend<{
|
|
52
|
+
destroy: () => void;
|
|
53
|
+
}>();
|
|
50
54
|
});
|
|
51
55
|
|
|
52
|
-
test("the
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
+
test("the entrypoint exposes the lazy wrapper and the widget itself", () => {
|
|
57
|
+
// The lazy wrapper is what works without a code-splitting bundler; the
|
|
58
|
+
// direct mount is what ProcaptchaFrictionless imports once it has already
|
|
59
|
+
// paid for the dynamic import of this package.
|
|
60
|
+
expectTypeOf<keyof typeof entrypoint>().toEqualTypeOf<
|
|
61
|
+
| "mountProcaptchaPuzzle"
|
|
62
|
+
| "loadProcaptchaPuzzle"
|
|
63
|
+
| "mountProcaptchaPuzzleWidget"
|
|
64
|
+
| "mountPuzzleCanvas"
|
|
65
|
+
>();
|
|
56
66
|
});
|
|
57
67
|
|
|
58
68
|
test("config, callbacks and i18n are all required", () => {
|
|
@@ -140,6 +150,7 @@ describe("Manager's types", () => {
|
|
|
140
150
|
});
|
|
141
151
|
|
|
142
152
|
describe("PuzzleCanvas' types", () => {
|
|
153
|
+
const translator = (): Translator => undefined as unknown as Translator;
|
|
143
154
|
const onComplete = (
|
|
144
155
|
_finalX: number,
|
|
145
156
|
_finalY: number,
|
|
@@ -148,9 +159,9 @@ describe("PuzzleCanvas' types", () => {
|
|
|
148
159
|
|
|
149
160
|
test("every prop is required, since none has a sensible default", () => {
|
|
150
161
|
// @ts-expect-error - a board with no imagery cannot be rendered.
|
|
151
|
-
|
|
162
|
+
mountPuzzleCanvas({ originX: 0, originY: 0 });
|
|
152
163
|
// @ts-expect-error - `submitting` gates the drag; omitting it unlocks it.
|
|
153
|
-
|
|
164
|
+
mountPuzzleCanvas({
|
|
154
165
|
originX: 0,
|
|
155
166
|
originY: 0,
|
|
156
167
|
background: "data:image/webp;base64,UklGRg==",
|
|
@@ -159,12 +170,13 @@ describe("PuzzleCanvas' types", () => {
|
|
|
159
170
|
onComplete,
|
|
160
171
|
showRetry: false,
|
|
161
172
|
theme: lightTheme,
|
|
173
|
+
translator: translator(),
|
|
162
174
|
});
|
|
163
175
|
});
|
|
164
176
|
|
|
165
|
-
test("the full prop set
|
|
177
|
+
test("the full prop set mounts a component that can be updated and torn down", () => {
|
|
166
178
|
expectTypeOf(
|
|
167
|
-
|
|
179
|
+
mountPuzzleCanvas({
|
|
168
180
|
originX: 0,
|
|
169
181
|
originY: 0,
|
|
170
182
|
background: "data:image/webp;base64,UklGRg==",
|
|
@@ -174,8 +186,12 @@ describe("PuzzleCanvas' types", () => {
|
|
|
174
186
|
showRetry: false,
|
|
175
187
|
submitting: false,
|
|
176
188
|
theme: lightTheme,
|
|
189
|
+
translator: translator(),
|
|
177
190
|
}),
|
|
178
|
-
).toExtend<
|
|
191
|
+
).toExtend<{
|
|
192
|
+
update: (props: PuzzleCanvasProps) => void;
|
|
193
|
+
destroy: () => void;
|
|
194
|
+
}>();
|
|
179
195
|
});
|
|
180
196
|
|
|
181
197
|
test("the drop is reported synchronously, not as a promise", () => {
|
|
@@ -14,34 +14,43 @@
|
|
|
14
14
|
|
|
15
15
|
import type { Ti18n } from "@prosopo/locale";
|
|
16
16
|
import type { ProcaptchaProps } from "@prosopo/types";
|
|
17
|
-
import { type ReactElement, act, createElement } from "react";
|
|
18
|
-
import { type Root, createRoot } from "react-dom/client";
|
|
19
17
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
18
|
+
import type { ProcaptchaPuzzleHandle } from "../components/procaptchaWidget.js";
|
|
20
19
|
import * as entrypoint from "../index.js";
|
|
20
|
+
import { type Mounted, mount, settle } from "./domHarness.js";
|
|
21
21
|
import { config } from "./managerHarness.js";
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* The outer
|
|
25
|
-
* it was given, so what is worth pinning down is that nothing is dropped
|
|
26
|
-
* way through — a wrapper that names props one at a time silently loses
|
|
27
|
-
* ones added later.
|
|
24
|
+
* The outer entry point exists only to lazy-load the widget and hand it the
|
|
25
|
+
* props it was given, so what is worth pinning down is that nothing is dropped
|
|
26
|
+
* on the way through — a wrapper that names props one at a time silently loses
|
|
27
|
+
* the ones added later.
|
|
28
28
|
*/
|
|
29
29
|
const mocks = vi.hoisted(() => ({
|
|
30
30
|
received: [] as ProcaptchaProps[],
|
|
31
|
+
destroyed: { count: 0 },
|
|
31
32
|
}));
|
|
32
33
|
|
|
33
|
-
vi.mock("../components/
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
vi.mock("../components/procaptchaWidget.js", () => ({
|
|
35
|
+
mountProcaptchaPuzzleWidget: (
|
|
36
|
+
container: HTMLElement,
|
|
37
|
+
props: ProcaptchaProps,
|
|
38
|
+
): ProcaptchaPuzzleHandle => {
|
|
39
|
+
mocks.received.push(props);
|
|
40
|
+
const element = document.createElement("div");
|
|
41
|
+
element.setAttribute("data-testid", "widget");
|
|
42
|
+
container.appendChild(element);
|
|
43
|
+
return {
|
|
44
|
+
destroy: () => {
|
|
45
|
+
element.remove();
|
|
46
|
+
mocks.destroyed.count += 1;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
}));
|
|
42
51
|
|
|
43
|
-
let
|
|
44
|
-
let
|
|
52
|
+
let mounted: Mounted;
|
|
53
|
+
let widget: ProcaptchaPuzzleHandle | undefined;
|
|
45
54
|
|
|
46
55
|
const props = (overrides: Partial<ProcaptchaProps> = {}): ProcaptchaProps => ({
|
|
47
56
|
config: config(),
|
|
@@ -51,33 +60,29 @@ const props = (overrides: Partial<ProcaptchaProps> = {}): ProcaptchaProps => ({
|
|
|
51
60
|
});
|
|
52
61
|
|
|
53
62
|
const render = async (widgetProps: ProcaptchaProps): Promise<void> => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
createElement(entrypoint.ProcaptchaPuzzle, widgetProps) as ReactElement,
|
|
57
|
-
);
|
|
58
|
-
});
|
|
63
|
+
widget = entrypoint.mountProcaptchaPuzzle(mounted.container, widgetProps);
|
|
64
|
+
await settle();
|
|
59
65
|
};
|
|
60
66
|
|
|
61
67
|
beforeEach(() => {
|
|
62
68
|
mocks.received.length = 0;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
root = createRoot(container);
|
|
67
|
-
});
|
|
69
|
+
mocks.destroyed.count = 0;
|
|
70
|
+
widget = undefined;
|
|
71
|
+
mounted = mount();
|
|
68
72
|
});
|
|
69
73
|
|
|
70
74
|
afterEach(() => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
container.remove();
|
|
75
|
+
widget?.destroy();
|
|
76
|
+
widget = undefined;
|
|
77
|
+
mounted.unmount();
|
|
75
78
|
});
|
|
76
79
|
|
|
77
|
-
describe("
|
|
80
|
+
describe("mountProcaptchaPuzzle", () => {
|
|
78
81
|
test("renders the widget once it has loaded", async () => {
|
|
79
82
|
await render(props());
|
|
80
|
-
expect(
|
|
83
|
+
expect(
|
|
84
|
+
mounted.container.querySelector('[data-testid="widget"]'),
|
|
85
|
+
).not.toBeNull();
|
|
81
86
|
});
|
|
82
87
|
|
|
83
88
|
test("passes on the props the widget needs but never names", async () => {
|
|
@@ -103,21 +108,31 @@ describe("ProcaptchaPuzzle", () => {
|
|
|
103
108
|
expect(mocks.received[0]?.config).toEqual(config());
|
|
104
109
|
});
|
|
105
110
|
|
|
106
|
-
test("
|
|
111
|
+
test("tears the loaded widget down", async () => {
|
|
107
112
|
await render(props());
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
widget?.destroy();
|
|
114
|
+
widget = undefined;
|
|
115
|
+
expect(mocks.destroyed.count).toBe(1);
|
|
116
|
+
expect(
|
|
117
|
+
mounted.container.querySelector('[data-testid="widget"]'),
|
|
118
|
+
).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("cancels a mount that is still loading", async () => {
|
|
122
|
+
// Otherwise a widget torn down mid-load reappears as an orphan that
|
|
123
|
+
// nothing holds a handle to.
|
|
124
|
+
widget = entrypoint.mountProcaptchaPuzzle(mounted.container, props());
|
|
125
|
+
widget.destroy();
|
|
126
|
+
widget = undefined;
|
|
127
|
+
await settle();
|
|
128
|
+
expect(mocks.received).toHaveLength(0);
|
|
112
129
|
});
|
|
113
130
|
});
|
|
114
131
|
|
|
115
132
|
describe("the package entrypoint", () => {
|
|
116
|
-
test("exports the
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
expect(typeof entrypoint.ProcaptchaPuzzle).toBe("function");
|
|
121
|
-
expect(Object.keys(entrypoint)).toEqual(["ProcaptchaPuzzle"]);
|
|
133
|
+
test("exports the mount functions a consumer needs", () => {
|
|
134
|
+
expect(typeof entrypoint.mountProcaptchaPuzzle).toBe("function");
|
|
135
|
+
expect(typeof entrypoint.loadProcaptchaPuzzle).toBe("function");
|
|
136
|
+
expect(typeof entrypoint.mountProcaptchaPuzzleWidget).toBe("function");
|
|
122
137
|
});
|
|
123
138
|
});
|