@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.
- 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 +41 -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
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// limitations under the License.
|
|
14
14
|
|
|
15
15
|
import type { Ti18n } from "@prosopo/locale";
|
|
16
|
+
import type { Component } from "@prosopo/procaptcha-common";
|
|
16
17
|
import {
|
|
17
18
|
type GetPuzzleCaptchaResponse,
|
|
18
19
|
ModeEnum,
|
|
@@ -20,8 +21,6 @@ import {
|
|
|
20
21
|
type ProcaptchaState,
|
|
21
22
|
type PuzzleEvent,
|
|
22
23
|
} from "@prosopo/types";
|
|
23
|
-
import { type ReactElement, act, createElement } from "react";
|
|
24
|
-
import { type Root, createRoot } from "react-dom/client";
|
|
25
24
|
import {
|
|
26
25
|
type Mock,
|
|
27
26
|
afterEach,
|
|
@@ -31,7 +30,12 @@ import {
|
|
|
31
30
|
test,
|
|
32
31
|
vi,
|
|
33
32
|
} from "vitest";
|
|
34
|
-
import
|
|
33
|
+
import {
|
|
34
|
+
type ProcaptchaPuzzleHandle,
|
|
35
|
+
mountProcaptchaPuzzleWidget,
|
|
36
|
+
} from "../components/procaptchaWidget.js";
|
|
37
|
+
import type { PuzzleCanvasProps } from "../components/puzzleCanvas.js";
|
|
38
|
+
import { type Mounted, fire, mount, settle } from "./domHarness.js";
|
|
35
39
|
import { challengeResponse, config, frictionless } from "./managerHarness.js";
|
|
36
40
|
|
|
37
41
|
/**
|
|
@@ -52,36 +56,9 @@ const mocks = vi.hoisted(() => {
|
|
|
52
56
|
getHoneypotValue?: () => string | undefined;
|
|
53
57
|
}[] = [];
|
|
54
58
|
const loadI18next = vi.fn<(a?: boolean, b?: string) => Promise<unknown>>();
|
|
55
|
-
const
|
|
56
|
-
current:
|
|
57
|
-
|
|
58
|
-
checked: boolean;
|
|
59
|
-
loading: boolean;
|
|
60
|
-
labelText: string;
|
|
61
|
-
error?: string;
|
|
62
|
-
onChange: (event: { nativeEvent: unknown }) => Promise<void>;
|
|
63
|
-
}
|
|
64
|
-
| undefined;
|
|
65
|
-
} = { current: undefined };
|
|
66
|
-
const canvasProps: {
|
|
67
|
-
current:
|
|
68
|
-
| {
|
|
69
|
-
originX: number;
|
|
70
|
-
originY: number;
|
|
71
|
-
background: string;
|
|
72
|
-
piece: string;
|
|
73
|
-
pieceSize: number;
|
|
74
|
-
showRetry: boolean;
|
|
75
|
-
submitting: boolean;
|
|
76
|
-
onComplete: (
|
|
77
|
-
x: number,
|
|
78
|
-
y: number,
|
|
79
|
-
events: PuzzleEvent[],
|
|
80
|
-
) => Promise<void> | void;
|
|
81
|
-
}
|
|
82
|
-
| undefined;
|
|
83
|
-
} = { current: undefined };
|
|
84
|
-
const honeypotQuestions: string[] = [];
|
|
59
|
+
const canvasProps: { current: PuzzleCanvasProps | undefined } = {
|
|
60
|
+
current: undefined,
|
|
61
|
+
};
|
|
85
62
|
const translationsReady = { current: true };
|
|
86
63
|
return {
|
|
87
64
|
translationsReady,
|
|
@@ -90,9 +67,7 @@ const mocks = vi.hoisted(() => {
|
|
|
90
67
|
resetState,
|
|
91
68
|
constructions,
|
|
92
69
|
loadI18next,
|
|
93
|
-
checkboxProps,
|
|
94
70
|
canvasProps,
|
|
95
|
-
honeypotQuestions,
|
|
96
71
|
};
|
|
97
72
|
});
|
|
98
73
|
|
|
@@ -117,79 +92,42 @@ vi.mock("../services/Manager.js", () => ({
|
|
|
117
92
|
// The canvas has its own suite; here it is reduced to a probe so a test can
|
|
118
93
|
// read the props the widget hands it and fire the completion callback without
|
|
119
94
|
// simulating a drag.
|
|
120
|
-
vi.mock("../components/
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
originY: number;
|
|
125
|
-
background: string;
|
|
126
|
-
piece: string;
|
|
127
|
-
pieceSize: number;
|
|
128
|
-
showRetry: boolean;
|
|
129
|
-
submitting: boolean;
|
|
130
|
-
onComplete: (
|
|
131
|
-
x: number,
|
|
132
|
-
y: number,
|
|
133
|
-
events: PuzzleEvent[],
|
|
134
|
-
) => Promise<void> | void;
|
|
135
|
-
}
|
|
136
|
-
const PuzzleCanvas = (canvasProps: CanvasStubProps) => {
|
|
95
|
+
vi.mock("../components/puzzleCanvas.js", () => ({
|
|
96
|
+
mountPuzzleCanvas: (
|
|
97
|
+
canvasProps: PuzzleCanvasProps,
|
|
98
|
+
): Component<PuzzleCanvasProps> => {
|
|
137
99
|
mocks.canvasProps.current = canvasProps;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
interface CheckboxStubProps {
|
|
153
|
-
checked: boolean;
|
|
154
|
-
loading: boolean;
|
|
155
|
-
labelText: string;
|
|
156
|
-
error?: string;
|
|
157
|
-
onChange: (event: { nativeEvent: unknown }) => Promise<void>;
|
|
158
|
-
}
|
|
159
|
-
const Checkbox = (checkboxProps: CheckboxStubProps) => {
|
|
160
|
-
mocks.checkboxProps.current = checkboxProps;
|
|
161
|
-
return create("input", {
|
|
162
|
-
type: "checkbox",
|
|
163
|
-
readOnly: true,
|
|
164
|
-
checked: checkboxProps.checked,
|
|
165
|
-
"aria-label": checkboxProps.labelText,
|
|
166
|
-
"data-error": checkboxProps.error,
|
|
167
|
-
"data-loading": String(checkboxProps.loading),
|
|
168
|
-
});
|
|
169
|
-
};
|
|
170
|
-
const Honeypot = forwardRef<HTMLInputElement, { encodedQuestion: string }>(
|
|
171
|
-
({ encodedQuestion }, ref) => {
|
|
172
|
-
mocks.honeypotQuestions.push(encodedQuestion);
|
|
173
|
-
return create("input", { type: "text", ref, name: "honeypot" });
|
|
174
|
-
},
|
|
175
|
-
);
|
|
176
|
-
return { ...actual, Checkbox, Honeypot };
|
|
177
|
-
});
|
|
100
|
+
const element = document.createElement("div");
|
|
101
|
+
element.setAttribute("data-cy", "canvas-stub");
|
|
102
|
+
document.body.appendChild(element);
|
|
103
|
+
return {
|
|
104
|
+
update: (next: PuzzleCanvasProps) => {
|
|
105
|
+
mocks.canvasProps.current = next;
|
|
106
|
+
},
|
|
107
|
+
destroy: () => {
|
|
108
|
+
element.remove();
|
|
109
|
+
mocks.canvasProps.current = undefined;
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
}));
|
|
178
114
|
|
|
179
115
|
vi.mock("@prosopo/locale", async (importOriginal) => {
|
|
180
116
|
const actual = await importOriginal<typeof import("@prosopo/locale")>();
|
|
181
117
|
return {
|
|
182
118
|
...actual,
|
|
183
119
|
loadI18next: mocks.loadI18next,
|
|
184
|
-
|
|
120
|
+
createTranslator: () => ({
|
|
185
121
|
t: (key: string) => key,
|
|
186
|
-
|
|
122
|
+
isReady: () => mocks.translationsReady.current,
|
|
123
|
+
subscribe: () => () => undefined,
|
|
124
|
+
i18n: undefined,
|
|
187
125
|
}),
|
|
188
126
|
};
|
|
189
127
|
});
|
|
190
128
|
|
|
191
|
-
let
|
|
192
|
-
let
|
|
129
|
+
let mounted: Mounted;
|
|
130
|
+
let widget: ProcaptchaPuzzleHandle | undefined;
|
|
193
131
|
|
|
194
132
|
const i18nStub = (
|
|
195
133
|
language: string,
|
|
@@ -204,53 +142,52 @@ const props = (overrides: Partial<ProcaptchaProps> = {}): ProcaptchaProps => ({
|
|
|
204
142
|
});
|
|
205
143
|
|
|
206
144
|
const render = (widgetProps: ProcaptchaProps): void => {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
145
|
+
widget = mountProcaptchaPuzzleWidget(mounted.container, widgetProps);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const destroy = (): void => {
|
|
149
|
+
widget?.destroy();
|
|
150
|
+
widget = undefined;
|
|
210
151
|
};
|
|
211
152
|
|
|
212
153
|
beforeEach(() => {
|
|
213
154
|
vi.clearAllMocks();
|
|
214
155
|
mocks.constructions.length = 0;
|
|
215
|
-
mocks.honeypotQuestions.length = 0;
|
|
216
156
|
mocks.translationsReady.current = true;
|
|
217
|
-
mocks.checkboxProps.current = undefined;
|
|
218
157
|
mocks.canvasProps.current = undefined;
|
|
219
158
|
mocks.start.mockResolvedValue(challengeResponse());
|
|
220
159
|
mocks.submitSolution.mockResolvedValue(true);
|
|
221
160
|
mocks.loadI18next.mockResolvedValue(undefined);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
act(() => {
|
|
225
|
-
root = createRoot(container);
|
|
226
|
-
});
|
|
161
|
+
widget = undefined;
|
|
162
|
+
mounted = mount();
|
|
227
163
|
});
|
|
228
164
|
|
|
229
165
|
afterEach(() => {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
});
|
|
233
|
-
container.remove();
|
|
234
|
-
// React schedules through timers, so a suite that leaves them faked makes
|
|
235
|
-
// every later render land after its assertions.
|
|
166
|
+
destroy();
|
|
167
|
+
mounted.unmount();
|
|
236
168
|
vi.useRealTimers();
|
|
237
169
|
vi.restoreAllMocks();
|
|
238
170
|
});
|
|
239
171
|
|
|
240
172
|
const checkbox = (): HTMLInputElement => {
|
|
241
|
-
const element = container.querySelector<HTMLInputElement>(
|
|
173
|
+
const element = mounted.container.querySelector<HTMLInputElement>(
|
|
242
174
|
'input[type="checkbox"]',
|
|
243
175
|
);
|
|
244
176
|
if (!element) throw new Error("expected a checkbox to be rendered");
|
|
245
177
|
return element;
|
|
246
178
|
};
|
|
247
179
|
|
|
180
|
+
// The canvas puts itself on the body rather than inside the widget, so it can
|
|
181
|
+
// escape the skeleton's query container.
|
|
248
182
|
const canvas = (): Element | null =>
|
|
249
|
-
|
|
183
|
+
document.body.querySelector('[data-cy="canvas-stub"]');
|
|
184
|
+
|
|
185
|
+
const spinner = (): Element | null =>
|
|
186
|
+
mounted.container.querySelector('[role="status"]');
|
|
250
187
|
|
|
251
188
|
const honeypotInput = (): HTMLInputElement => {
|
|
252
|
-
const element =
|
|
253
|
-
'input[name="
|
|
189
|
+
const element = document.querySelector<HTMLInputElement>(
|
|
190
|
+
'input[name="email_confirm"]',
|
|
254
191
|
);
|
|
255
192
|
if (!element) throw new Error("expected a honeypot to be rendered");
|
|
256
193
|
return element;
|
|
@@ -263,17 +200,10 @@ interface ClickOptions {
|
|
|
263
200
|
touches?: { clientX: number; clientY: number }[];
|
|
264
201
|
}
|
|
265
202
|
|
|
266
|
-
/**
|
|
203
|
+
/** Click the real checkbox with the browser event a user would produce. */
|
|
267
204
|
const click = async (options: ClickOptions = {}): Promise<void> => {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
clientX: options.clientX ?? 0,
|
|
271
|
-
clientY: options.clientY ?? 0,
|
|
272
|
-
...(options.touches ? { touches: options.touches } : {}),
|
|
273
|
-
};
|
|
274
|
-
await act(async () => {
|
|
275
|
-
await mocks.checkboxProps.current?.onChange({ nativeEvent });
|
|
276
|
-
});
|
|
205
|
+
fire(checkbox(), "click", options);
|
|
206
|
+
await settle();
|
|
277
207
|
};
|
|
278
208
|
|
|
279
209
|
/**
|
|
@@ -281,11 +211,7 @@ const click = async (options: ClickOptions = {}): Promise<void> => {
|
|
|
281
211
|
* manager's promise open.
|
|
282
212
|
*/
|
|
283
213
|
const clickWithoutWaiting = (): void => {
|
|
284
|
-
|
|
285
|
-
void mocks.checkboxProps.current?.onChange({
|
|
286
|
-
nativeEvent: { isTrusted: true, clientX: 0, clientY: 0 },
|
|
287
|
-
});
|
|
288
|
-
});
|
|
214
|
+
fire(checkbox(), "click");
|
|
289
215
|
};
|
|
290
216
|
|
|
291
217
|
/** Fire the canvas completion callback the way a finished drag would. */
|
|
@@ -294,9 +220,8 @@ const complete = async (
|
|
|
294
220
|
y = 80,
|
|
295
221
|
events: PuzzleEvent[] = [{ x: 200, y: 80, t: 5 }],
|
|
296
222
|
): Promise<void> => {
|
|
297
|
-
await
|
|
298
|
-
|
|
299
|
-
});
|
|
223
|
+
await mocks.canvasProps.current?.onComplete(x, y, events);
|
|
224
|
+
await settle();
|
|
300
225
|
};
|
|
301
226
|
|
|
302
227
|
describe("what the widget renders", () => {
|
|
@@ -307,7 +232,9 @@ describe("what the widget renders", () => {
|
|
|
307
232
|
|
|
308
233
|
test("an invisible widget shows no checkbox at all", () => {
|
|
309
234
|
render(props({ config: config({ mode: ModeEnum.invisible }) }));
|
|
310
|
-
expect(
|
|
235
|
+
expect(
|
|
236
|
+
mounted.container.querySelector('input[type="checkbox"]'),
|
|
237
|
+
).toBeNull();
|
|
311
238
|
});
|
|
312
239
|
|
|
313
240
|
test("no puzzle is shown until a challenge has been fetched", () => {
|
|
@@ -316,18 +243,22 @@ describe("what the widget renders", () => {
|
|
|
316
243
|
});
|
|
317
244
|
|
|
318
245
|
test("a session with a honeypot question renders the honeypot", () => {
|
|
319
|
-
render(
|
|
246
|
+
render(
|
|
247
|
+
props({ frictionlessState: frictionless({ hp: btoa("question") }) }),
|
|
248
|
+
);
|
|
320
249
|
expect(honeypotInput()).toBeDefined();
|
|
321
|
-
expect(
|
|
250
|
+
expect(document.body.textContent).toContain("question");
|
|
322
251
|
});
|
|
323
252
|
|
|
324
253
|
test("a session without one renders no bait at all", () => {
|
|
325
254
|
render(props({ frictionlessState: frictionless() }));
|
|
326
|
-
expect(
|
|
255
|
+
expect(document.querySelector('input[name="email_confirm"]')).toBeNull();
|
|
327
256
|
});
|
|
328
257
|
|
|
329
258
|
test("the manager can read the honeypot input the widget rendered", () => {
|
|
330
|
-
render(
|
|
259
|
+
render(
|
|
260
|
+
props({ frictionlessState: frictionless({ hp: btoa("question") }) }),
|
|
261
|
+
);
|
|
331
262
|
honeypotInput().value = "bot@example.com";
|
|
332
263
|
expect(mocks.constructions[0]?.getHoneypotValue?.()).toBe(
|
|
333
264
|
"bot@example.com",
|
|
@@ -341,32 +272,22 @@ describe("what the widget renders", () => {
|
|
|
341
272
|
|
|
342
273
|
test("the checkbox is labelled once the translations are ready", () => {
|
|
343
274
|
render(props());
|
|
344
|
-
expect(
|
|
275
|
+
expect(checkbox().getAttribute("aria-label")).toBe("WIDGET.I_AM_HUMAN");
|
|
345
276
|
});
|
|
346
277
|
|
|
347
278
|
test("the label is left empty while the translations load", () => {
|
|
348
279
|
mocks.translationsReady.current = false;
|
|
349
280
|
render(props());
|
|
350
|
-
expect(
|
|
281
|
+
expect(checkbox().getAttribute("aria-label")).toBe("");
|
|
351
282
|
});
|
|
352
283
|
|
|
353
|
-
test("the widget
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
// the
|
|
357
|
-
render(props());
|
|
284
|
+
test("the widget builds exactly one manager", async () => {
|
|
285
|
+
// The React version rebuilt the manager on every render and kept the
|
|
286
|
+
// first through a ref, which is what lost the checkbox click coordinates
|
|
287
|
+
// before the ref was introduced. There is one manager per mount now.
|
|
358
288
|
render(props());
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
error: {
|
|
362
|
-
message: "from the original manager",
|
|
363
|
-
key: "API.UNKNOWN_ERROR",
|
|
364
|
-
},
|
|
365
|
-
});
|
|
366
|
-
});
|
|
367
|
-
expect(mocks.checkboxProps.current?.error).toBe(
|
|
368
|
-
"from the original manager",
|
|
369
|
-
);
|
|
289
|
+
await click();
|
|
290
|
+
expect(mocks.constructions).toHaveLength(1);
|
|
370
291
|
});
|
|
371
292
|
});
|
|
372
293
|
|
|
@@ -419,10 +340,12 @@ describe("starting from the checkbox", () => {
|
|
|
419
340
|
});
|
|
420
341
|
});
|
|
421
342
|
|
|
422
|
-
test("
|
|
343
|
+
test("a synthetic click never reaches the manager", async () => {
|
|
344
|
+
// The checkbox drops untrusted events before the widget sees them, so an
|
|
345
|
+
// automated solver cannot open a puzzle at all.
|
|
423
346
|
render(props());
|
|
424
347
|
await click({ trusted: false, clientX: 12, clientY: 34 });
|
|
425
|
-
expect(mocks.start).
|
|
348
|
+
expect(mocks.start).not.toHaveBeenCalled();
|
|
426
349
|
});
|
|
427
350
|
|
|
428
351
|
test("a tap reports the coordinates of the first touch", async () => {
|
|
@@ -431,7 +354,7 @@ describe("starting from the checkbox", () => {
|
|
|
431
354
|
expect(mocks.start).toHaveBeenCalledWith(7, 9);
|
|
432
355
|
});
|
|
433
356
|
|
|
434
|
-
test("a touch event with no touches falls back to the
|
|
357
|
+
test("a touch event with no touches falls back to the pointer", async () => {
|
|
435
358
|
render(props());
|
|
436
359
|
await click({ touches: [] });
|
|
437
360
|
expect(mocks.start).toHaveBeenCalledWith(0, 0);
|
|
@@ -442,9 +365,11 @@ describe("starting from the checkbox", () => {
|
|
|
442
365
|
// replaced synchronously by the promise executor below
|
|
443
366
|
};
|
|
444
367
|
mocks.start.mockReturnValue(
|
|
445
|
-
new Promise<GetPuzzleCaptchaResponse>(
|
|
446
|
-
|
|
447
|
-
|
|
368
|
+
new Promise<GetPuzzleCaptchaResponse>(
|
|
369
|
+
(resolve: (challenge: GetPuzzleCaptchaResponse) => void) => {
|
|
370
|
+
release = resolve;
|
|
371
|
+
},
|
|
372
|
+
),
|
|
448
373
|
);
|
|
449
374
|
render(props());
|
|
450
375
|
// Not awaited: the handler cannot settle until the challenge is
|
|
@@ -452,9 +377,8 @@ describe("starting from the checkbox", () => {
|
|
|
452
377
|
clickWithoutWaiting();
|
|
453
378
|
clickWithoutWaiting();
|
|
454
379
|
expect(mocks.start).toHaveBeenCalledTimes(1);
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
});
|
|
380
|
+
release(challengeResponse());
|
|
381
|
+
await settle();
|
|
458
382
|
});
|
|
459
383
|
|
|
460
384
|
test("the checkbox shows a spinner while the solve is loading", async () => {
|
|
@@ -462,17 +386,19 @@ describe("starting from the checkbox", () => {
|
|
|
462
386
|
// replaced synchronously by the promise executor below
|
|
463
387
|
};
|
|
464
388
|
mocks.start.mockReturnValue(
|
|
465
|
-
new Promise<GetPuzzleCaptchaResponse>(
|
|
466
|
-
|
|
467
|
-
|
|
389
|
+
new Promise<GetPuzzleCaptchaResponse>(
|
|
390
|
+
(resolve: (challenge: GetPuzzleCaptchaResponse) => void) => {
|
|
391
|
+
release = resolve;
|
|
392
|
+
},
|
|
393
|
+
),
|
|
468
394
|
);
|
|
469
395
|
render(props());
|
|
470
396
|
clickWithoutWaiting();
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
expect(
|
|
397
|
+
await settle();
|
|
398
|
+
expect(spinner()).not.toBeNull();
|
|
399
|
+
release(challengeResponse());
|
|
400
|
+
await settle();
|
|
401
|
+
expect(spinner()).toBeNull();
|
|
476
402
|
});
|
|
477
403
|
|
|
478
404
|
test("a start that yields no challenge leaves the checkbox in place", async () => {
|
|
@@ -480,7 +406,7 @@ describe("starting from the checkbox", () => {
|
|
|
480
406
|
render(props());
|
|
481
407
|
await click();
|
|
482
408
|
expect(canvas()).toBeNull();
|
|
483
|
-
expect(
|
|
409
|
+
expect(spinner()).toBeNull();
|
|
484
410
|
});
|
|
485
411
|
|
|
486
412
|
test("a start that throws returns the user to a usable checkbox", async () => {
|
|
@@ -490,7 +416,7 @@ describe("starting from the checkbox", () => {
|
|
|
490
416
|
await click();
|
|
491
417
|
// Without the guard the spinner would stay up for good: nothing awaits
|
|
492
418
|
// the checkbox handler, so the rejection escapes as an unhandled one.
|
|
493
|
-
expect(
|
|
419
|
+
expect(spinner()).toBeNull();
|
|
494
420
|
expect(onError).toHaveBeenCalledWith(expect.any(Error));
|
|
495
421
|
});
|
|
496
422
|
|
|
@@ -498,42 +424,30 @@ describe("starting from the checkbox", () => {
|
|
|
498
424
|
mocks.start.mockRejectedValue(new Error("provider down"));
|
|
499
425
|
render(props());
|
|
500
426
|
await click();
|
|
501
|
-
expect(
|
|
427
|
+
expect(spinner()).toBeNull();
|
|
502
428
|
});
|
|
503
429
|
});
|
|
504
430
|
|
|
505
431
|
describe("autoStart", () => {
|
|
506
432
|
test("fetches a challenge and opens the puzzle without a click", async () => {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
createElement(
|
|
510
|
-
Procaptcha,
|
|
511
|
-
props({ autoStart: true, startCoords: { x: 5, y: 6 } }),
|
|
512
|
-
) as ReactElement,
|
|
513
|
-
);
|
|
514
|
-
});
|
|
433
|
+
render(props({ autoStart: true, startCoords: { x: 5, y: 6 } }));
|
|
434
|
+
await settle();
|
|
515
435
|
expect(mocks.start).toHaveBeenCalledWith(5, 6);
|
|
516
436
|
expect(canvas()).not.toBeNull();
|
|
517
437
|
});
|
|
518
438
|
|
|
519
439
|
test("starts at the origin when no coordinates were handed over", async () => {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
createElement(Procaptcha, props({ autoStart: true })) as ReactElement,
|
|
523
|
-
);
|
|
524
|
-
});
|
|
440
|
+
render(props({ autoStart: true }));
|
|
441
|
+
await settle();
|
|
525
442
|
expect(mocks.start).toHaveBeenCalledWith(0, 0);
|
|
526
443
|
});
|
|
527
444
|
|
|
528
445
|
test("a failed autoStart leaves the checkbox usable", async () => {
|
|
529
446
|
mocks.start.mockRejectedValue(new Error("nope"));
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
createElement(Procaptcha, props({ autoStart: true })) as ReactElement,
|
|
533
|
-
);
|
|
534
|
-
});
|
|
447
|
+
render(props({ autoStart: true }));
|
|
448
|
+
await settle();
|
|
535
449
|
expect(canvas()).toBeNull();
|
|
536
|
-
expect(
|
|
450
|
+
expect(spinner()).toBeNull();
|
|
537
451
|
});
|
|
538
452
|
|
|
539
453
|
test("no autoStart means no solve until the user acts", () => {
|
|
@@ -561,7 +475,7 @@ describe("finishing the drag", () => {
|
|
|
561
475
|
await openPuzzle();
|
|
562
476
|
await complete();
|
|
563
477
|
expect(canvas()).toBeNull();
|
|
564
|
-
expect(
|
|
478
|
+
expect(spinner()).toBeNull();
|
|
565
479
|
});
|
|
566
480
|
|
|
567
481
|
test("a rejected solution asks for another go on a fresh challenge", async () => {
|
|
@@ -628,29 +542,25 @@ describe("finishing the drag", () => {
|
|
|
628
542
|
// replaced synchronously by the promise executor below
|
|
629
543
|
};
|
|
630
544
|
mocks.submitSolution.mockReturnValue(
|
|
631
|
-
new Promise<boolean>((resolve) => {
|
|
545
|
+
new Promise<boolean>((resolve: (verified: boolean) => void) => {
|
|
632
546
|
release = resolve;
|
|
633
547
|
}),
|
|
634
548
|
);
|
|
635
549
|
await openPuzzle();
|
|
636
550
|
// Not awaited: the handler cannot settle until the verdict is released.
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
});
|
|
551
|
+
void mocks.canvasProps.current?.onComplete(200, 80, []);
|
|
552
|
+
await settle();
|
|
640
553
|
expect(mocks.canvasProps.current?.submitting).toBe(true);
|
|
641
|
-
expect(
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
});
|
|
554
|
+
expect(spinner()).not.toBeNull();
|
|
555
|
+
release(true);
|
|
556
|
+
await settle();
|
|
645
557
|
});
|
|
646
558
|
});
|
|
647
559
|
|
|
648
560
|
describe("invisible mode", () => {
|
|
649
561
|
const execute = async (): Promise<void> => {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
await Promise.resolve();
|
|
653
|
-
});
|
|
562
|
+
document.dispatchEvent(new Event("procaptcha:execute"));
|
|
563
|
+
await settle();
|
|
654
564
|
};
|
|
655
565
|
|
|
656
566
|
test("an execute event fetches a challenge and opens the puzzle", async () => {
|
|
@@ -669,10 +579,8 @@ describe("invisible mode", () => {
|
|
|
669
579
|
test("a targeted execute on the container runs a visible widget", async () => {
|
|
670
580
|
const target = document.createElement("div");
|
|
671
581
|
render(props({ container: target }));
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
await Promise.resolve();
|
|
675
|
-
});
|
|
582
|
+
target.dispatchEvent(new Event("procaptcha:execute"));
|
|
583
|
+
await settle();
|
|
676
584
|
expect(mocks.start).toHaveBeenCalledTimes(1);
|
|
677
585
|
expect(canvas()).not.toBeNull();
|
|
678
586
|
});
|
|
@@ -685,10 +593,8 @@ describe("invisible mode", () => {
|
|
|
685
593
|
container: target,
|
|
686
594
|
}),
|
|
687
595
|
);
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
await Promise.resolve();
|
|
691
|
-
});
|
|
596
|
+
target.dispatchEvent(new Event("procaptcha:execute"));
|
|
597
|
+
await settle();
|
|
692
598
|
expect(mocks.start).toHaveBeenCalledTimes(1);
|
|
693
599
|
});
|
|
694
600
|
|
|
@@ -725,43 +631,37 @@ describe("invisible mode", () => {
|
|
|
725
631
|
expect(onError).toHaveBeenCalledWith(expect.any(Error));
|
|
726
632
|
});
|
|
727
633
|
|
|
728
|
-
test("the listener is dropped when the widget
|
|
634
|
+
test("the listener is dropped when the widget is destroyed", async () => {
|
|
729
635
|
render(props({ config: config({ mode: ModeEnum.invisible }) }));
|
|
730
|
-
|
|
731
|
-
root.unmount();
|
|
732
|
-
});
|
|
733
|
-
act(() => {
|
|
734
|
-
root = createRoot(container);
|
|
735
|
-
});
|
|
636
|
+
destroy();
|
|
736
637
|
await execute();
|
|
737
638
|
expect(mocks.start).not.toHaveBeenCalled();
|
|
738
639
|
});
|
|
739
640
|
});
|
|
740
641
|
|
|
741
642
|
describe("an invalidated session", () => {
|
|
742
|
-
const invalidate = (
|
|
643
|
+
const invalidate = async (
|
|
743
644
|
key = "CAPTCHA.NO_SESSION_FOUND",
|
|
744
645
|
message = "session gone",
|
|
745
|
-
): void => {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
});
|
|
646
|
+
): Promise<void> => {
|
|
647
|
+
mocks.constructions[0]?.updateState({ error: { message, key } });
|
|
648
|
+
await settle();
|
|
749
649
|
};
|
|
750
650
|
|
|
751
651
|
test("the error is shown on the checkbox and the puzzle is torn down", async () => {
|
|
752
652
|
render(props());
|
|
753
653
|
await click();
|
|
754
|
-
invalidate("API.UNKNOWN_ERROR", "something broke");
|
|
755
|
-
expect(
|
|
654
|
+
await invalidate("API.UNKNOWN_ERROR", "something broke");
|
|
655
|
+
expect(mounted.container.textContent).toContain("something broke");
|
|
756
656
|
expect(canvas()).toBeNull();
|
|
757
|
-
expect(
|
|
657
|
+
expect(spinner()).toBeNull();
|
|
758
658
|
});
|
|
759
659
|
|
|
760
660
|
test("the host is told to re-mint, with the coordinates of the original click", async () => {
|
|
761
661
|
const onSessionInvalidated = vi.fn<(x?: number, y?: number) => void>();
|
|
762
662
|
render(props({ onSessionInvalidated }));
|
|
763
663
|
await click({ clientX: 12, clientY: 34 });
|
|
764
|
-
invalidate();
|
|
664
|
+
await invalidate();
|
|
765
665
|
expect(onSessionInvalidated).toHaveBeenCalledWith(12, 34);
|
|
766
666
|
});
|
|
767
667
|
|
|
@@ -769,52 +669,39 @@ describe("an invalidated session", () => {
|
|
|
769
669
|
const onSessionInvalidated = vi.fn<(x?: number, y?: number) => void>();
|
|
770
670
|
render(props({ onSessionInvalidated }));
|
|
771
671
|
await click();
|
|
772
|
-
invalidate("CAPTCHA.NO_SESSION_FOUND", "gone");
|
|
773
|
-
invalidate("CAPTCHA.NO_SESSION_FOUND", "gone again");
|
|
672
|
+
await invalidate("CAPTCHA.NO_SESSION_FOUND", "gone");
|
|
673
|
+
await invalidate("CAPTCHA.NO_SESSION_FOUND", "gone again");
|
|
774
674
|
expect(onSessionInvalidated).toHaveBeenCalledTimes(1);
|
|
775
675
|
});
|
|
776
676
|
|
|
777
677
|
test("with no host handler the frictionless session restarts instead", async () => {
|
|
778
|
-
vi.useFakeTimers();
|
|
779
678
|
const restart = vi.fn<() => void>();
|
|
780
679
|
render(props({ frictionlessState: frictionless({ restart }) }));
|
|
781
|
-
invalidate();
|
|
680
|
+
await invalidate();
|
|
782
681
|
expect(restart).not.toHaveBeenCalled();
|
|
783
|
-
await
|
|
784
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
785
|
-
});
|
|
682
|
+
await new Promise<void>((resolve: () => void) => setTimeout(resolve, 150));
|
|
786
683
|
expect(restart).toHaveBeenCalledTimes(1);
|
|
787
|
-
vi.useRealTimers();
|
|
788
684
|
});
|
|
789
685
|
|
|
790
|
-
test("a restart pending at
|
|
791
|
-
vi.useFakeTimers();
|
|
686
|
+
test("a restart pending at destroy is cancelled", async () => {
|
|
792
687
|
const restart = vi.fn<() => void>();
|
|
793
688
|
render(props({ frictionlessState: frictionless({ restart }) }));
|
|
794
|
-
invalidate();
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
});
|
|
798
|
-
await act(async () => {
|
|
799
|
-
await vi.advanceTimersByTimeAsync(200);
|
|
800
|
-
});
|
|
689
|
+
await invalidate();
|
|
690
|
+
destroy();
|
|
691
|
+
await new Promise<void>((resolve: () => void) => setTimeout(resolve, 200));
|
|
801
692
|
expect(restart).not.toHaveBeenCalled();
|
|
802
|
-
act(() => {
|
|
803
|
-
root = createRoot(container);
|
|
804
|
-
});
|
|
805
|
-
vi.useRealTimers();
|
|
806
693
|
});
|
|
807
694
|
|
|
808
695
|
test("a lost session with nothing to recover it is simply surfaced", async () => {
|
|
809
696
|
render(props());
|
|
810
|
-
invalidate();
|
|
811
|
-
expect(
|
|
697
|
+
await invalidate();
|
|
698
|
+
expect(mounted.container.textContent).toContain("session gone");
|
|
812
699
|
});
|
|
813
700
|
|
|
814
|
-
test("no coordinates are reported when the session was never clicked into", () => {
|
|
701
|
+
test("no coordinates are reported when the session was never clicked into", async () => {
|
|
815
702
|
const onSessionInvalidated = vi.fn<(x?: number, y?: number) => void>();
|
|
816
703
|
render(props({ onSessionInvalidated }));
|
|
817
|
-
invalidate();
|
|
704
|
+
await invalidate();
|
|
818
705
|
expect(onSessionInvalidated).toHaveBeenCalledWith(undefined, undefined);
|
|
819
706
|
});
|
|
820
707
|
});
|