@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
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
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 { Ti18n } from "@prosopo/locale";
|
|
16
|
+
import {
|
|
17
|
+
type GetPuzzleCaptchaResponse,
|
|
18
|
+
ModeEnum,
|
|
19
|
+
type ProcaptchaProps,
|
|
20
|
+
type ProcaptchaState,
|
|
21
|
+
type PuzzleEvent,
|
|
22
|
+
} from "@prosopo/types";
|
|
23
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
24
|
+
import {
|
|
25
|
+
type ProcaptchaPuzzleHandle,
|
|
26
|
+
mountProcaptchaPuzzleWidget,
|
|
27
|
+
} from "../components/procaptchaWidget.js";
|
|
28
|
+
import { type Mounted, fire, mount, settle } from "./domHarness.js";
|
|
29
|
+
import { challengeResponse, config, frictionless } from "./managerHarness.js";
|
|
30
|
+
|
|
31
|
+
// The canvas is deliberately NOT mocked here: the point of this suite is
|
|
32
|
+
// whether the overlay the user has to drag actually becomes visible. The
|
|
33
|
+
// widget suite stubs the canvas, so it asserts the challenge was handed over,
|
|
34
|
+
// not that anything reached the screen.
|
|
35
|
+
const mocks = vi.hoisted(() => {
|
|
36
|
+
const start =
|
|
37
|
+
vi.fn<
|
|
38
|
+
(x?: number, y?: number) => Promise<GetPuzzleCaptchaResponse | undefined>
|
|
39
|
+
>();
|
|
40
|
+
const submitSolution =
|
|
41
|
+
vi.fn<(x: number, y: number, events: PuzzleEvent[]) => Promise<boolean>>();
|
|
42
|
+
const resetState = vi.fn<() => void>();
|
|
43
|
+
return { start, submitSolution, resetState };
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
vi.mock("../services/Manager.js", () => ({
|
|
47
|
+
Manager: (
|
|
48
|
+
_config: unknown,
|
|
49
|
+
_state: ProcaptchaState,
|
|
50
|
+
_updateState: (next: Partial<ProcaptchaState>) => void,
|
|
51
|
+
) => ({
|
|
52
|
+
start: mocks.start,
|
|
53
|
+
submitSolution: mocks.submitSolution,
|
|
54
|
+
resetState: mocks.resetState,
|
|
55
|
+
}),
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
vi.mock("@prosopo/locale", async (importOriginal) => ({
|
|
59
|
+
...(await importOriginal<typeof import("@prosopo/locale")>()),
|
|
60
|
+
loadI18next: vi.fn(async () => undefined),
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
const i18nStub = {
|
|
64
|
+
isInitialized: true,
|
|
65
|
+
language: "en",
|
|
66
|
+
t: (key: string) => key,
|
|
67
|
+
changeLanguage: vi.fn(),
|
|
68
|
+
hasLoadedNamespace: () => true,
|
|
69
|
+
on: () => undefined,
|
|
70
|
+
off: () => undefined,
|
|
71
|
+
} as unknown as Ti18n;
|
|
72
|
+
|
|
73
|
+
const props = (overrides: Partial<ProcaptchaProps> = {}): ProcaptchaProps =>
|
|
74
|
+
({
|
|
75
|
+
config: config({ mode: ModeEnum.visible }),
|
|
76
|
+
callbacks: {},
|
|
77
|
+
i18n: i18nStub,
|
|
78
|
+
frictionlessState: frictionless(),
|
|
79
|
+
...overrides,
|
|
80
|
+
}) as unknown as ProcaptchaProps;
|
|
81
|
+
|
|
82
|
+
let mounted: Mounted;
|
|
83
|
+
let handle: ProcaptchaPuzzleHandle | undefined;
|
|
84
|
+
|
|
85
|
+
// Class names are hashed by the CSS-module build, so the surface is addressed
|
|
86
|
+
// through the `data-cy` hooks the components ship for exactly this reason.
|
|
87
|
+
const surface = (): HTMLElement | null =>
|
|
88
|
+
document.querySelector<HTMLElement>('[data-cy="challenge-surface"]');
|
|
89
|
+
|
|
90
|
+
// The panel carrying the entrance animation is the only element under the
|
|
91
|
+
// surface with an inline opacity of its own.
|
|
92
|
+
const panelOpacity = (): string | undefined => {
|
|
93
|
+
const content = document.querySelector('[data-cy="challenge-content"]');
|
|
94
|
+
if (!content) return undefined;
|
|
95
|
+
return Array.from(
|
|
96
|
+
content.querySelectorAll<HTMLElement>("[style*='opacity']"),
|
|
97
|
+
).at(-1)?.style.opacity;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// The entrance animation flips opacity inside a requestAnimationFrame, so a
|
|
101
|
+
// test has to let a frame run before reading it.
|
|
102
|
+
const frame = async (): Promise<void> => {
|
|
103
|
+
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
104
|
+
await settle();
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
mocks.start.mockResolvedValue(challengeResponse());
|
|
109
|
+
mounted = mount();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
afterEach(() => {
|
|
113
|
+
handle?.destroy();
|
|
114
|
+
handle = undefined;
|
|
115
|
+
mounted.unmount();
|
|
116
|
+
for (const layer of Array.from(
|
|
117
|
+
document.querySelectorAll(".prosopo-challenge-surface"),
|
|
118
|
+
)) {
|
|
119
|
+
layer.remove();
|
|
120
|
+
}
|
|
121
|
+
vi.clearAllMocks();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("the puzzle overlay actually reaches the screen", () => {
|
|
125
|
+
test("visible mode: the panel is opaque after the entrance frame", async () => {
|
|
126
|
+
handle = mountProcaptchaPuzzleWidget(mounted.container, props());
|
|
127
|
+
const checkbox = mounted.container.querySelector("input");
|
|
128
|
+
expect(checkbox, "the visible widget renders a checkbox").not.toBeNull();
|
|
129
|
+
if (checkbox) fire(checkbox, "click");
|
|
130
|
+
await settle();
|
|
131
|
+
await frame();
|
|
132
|
+
|
|
133
|
+
expect(mocks.start).toHaveBeenCalledTimes(1);
|
|
134
|
+
expect(surface(), "the overlay is mounted").not.toBeNull();
|
|
135
|
+
expect(panelOpacity()).toBe("1");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Invisible mode has no checkbox, so the overlay is opened by the host
|
|
139
|
+
// page's execute(). It still has to reach the screen: the piece is dragged
|
|
140
|
+
// by hand, and a panel left at opacity 0 is a challenge nobody can solve.
|
|
141
|
+
test("invisible mode: the panel is opaque after the entrance frame", async () => {
|
|
142
|
+
handle = mountProcaptchaPuzzleWidget(
|
|
143
|
+
mounted.container,
|
|
144
|
+
props({ config: config({ mode: ModeEnum.invisible }) }),
|
|
145
|
+
);
|
|
146
|
+
document.dispatchEvent(new Event("procaptcha:execute"));
|
|
147
|
+
await settle();
|
|
148
|
+
await frame();
|
|
149
|
+
|
|
150
|
+
expect(mocks.start).toHaveBeenCalledTimes(1);
|
|
151
|
+
expect(surface(), "the overlay is mounted").not.toBeNull();
|
|
152
|
+
expect(panelOpacity()).toBe("1");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// The entrance frame is cancelled on teardown, so a canvas destroyed and
|
|
156
|
+
// remounted before it runs would stay transparent for good.
|
|
157
|
+
test("invisible mode: a remounted overlay still becomes opaque", async () => {
|
|
158
|
+
handle = mountProcaptchaPuzzleWidget(
|
|
159
|
+
mounted.container,
|
|
160
|
+
props({ config: config({ mode: ModeEnum.invisible }) }),
|
|
161
|
+
);
|
|
162
|
+
document.dispatchEvent(new Event("procaptcha:execute"));
|
|
163
|
+
await settle();
|
|
164
|
+
await frame();
|
|
165
|
+
expect(panelOpacity()).toBe("1");
|
|
166
|
+
|
|
167
|
+
handle.destroy();
|
|
168
|
+
handle = mountProcaptchaPuzzleWidget(
|
|
169
|
+
mounted.container,
|
|
170
|
+
props({ config: config({ mode: ModeEnum.invisible }) }),
|
|
171
|
+
);
|
|
172
|
+
document.dispatchEvent(new Event("procaptcha:execute"));
|
|
173
|
+
await settle();
|
|
174
|
+
await frame();
|
|
175
|
+
|
|
176
|
+
expect(surface(), "the overlay is mounted again").not.toBeNull();
|
|
177
|
+
expect(panelOpacity()).toBe("1");
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -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
|
});
|