@prosopo/procaptcha-pow 2.12.7 → 2.13.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 +5 -5
- package/.turbo/turbo-build$colon$tsc.log +20 -20
- package/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +62 -0
- package/dist/cjs/components/procaptchaPoW.cjs +22 -0
- package/dist/cjs/components/procaptchaWidget.cjs +143 -0
- package/dist/cjs/index.cjs +5 -2
- package/dist/components/procaptchaPoW.d.ts +7 -0
- package/dist/components/procaptchaPoW.d.ts.map +1 -0
- package/dist/components/procaptchaPoW.js +21 -0
- package/dist/components/procaptchaPoW.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 +143 -0
- package/dist/components/procaptchaWidget.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -2
- 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/procaptchaPoW.unit.test.js +46 -30
- package/dist/tests/procaptchaPoW.unit.test.js.map +1 -1
- package/dist/tests/procaptchaPow.test-d.js +5 -5
- package/dist/tests/procaptchaPow.test-d.js.map +1 -1
- package/dist/tests/procaptchaWidget.unit.test.js +79 -137
- package/dist/tests/procaptchaWidget.unit.test.js.map +1 -1
- package/package.json +11 -15
- package/src/components/procaptchaPoW.ts +53 -0
- package/src/components/procaptchaWidget.ts +281 -0
- package/src/index.ts +2 -2
- package/src/tests/domHarness.ts +122 -0
- package/src/tests/procaptchaPoW.unit.test.ts +59 -36
- package/src/tests/procaptchaPow.test-d.ts +15 -9
- package/src/tests/procaptchaWidget.unit.test.ts +105 -171
- package/tsconfig.cjs.json +1 -2
- package/tsconfig.json +1 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/components/ProcaptchaPoW.cjs +0 -7
- package/dist/cjs/components/ProcaptchaWidget.cjs +0 -130
- package/dist/components/ProcaptchaPoW.d.ts +0 -3
- package/dist/components/ProcaptchaPoW.d.ts.map +0 -1
- package/dist/components/ProcaptchaPoW.js +0 -7
- package/dist/components/ProcaptchaPoW.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 -131
- package/dist/components/ProcaptchaWidget.js.map +0 -1
- package/src/components/ProcaptchaPoW.tsx +0 -39
- package/src/components/ProcaptchaWidget.tsx +0 -231
|
@@ -18,8 +18,6 @@ import {
|
|
|
18
18
|
type ProcaptchaProps,
|
|
19
19
|
type ProcaptchaState,
|
|
20
20
|
} from "@prosopo/types";
|
|
21
|
-
import { type ReactElement, act, createElement } from "react";
|
|
22
|
-
import { type Root, createRoot } from "react-dom/client";
|
|
23
21
|
import {
|
|
24
22
|
type Mock,
|
|
25
23
|
afterEach,
|
|
@@ -29,7 +27,11 @@ import {
|
|
|
29
27
|
test,
|
|
30
28
|
vi,
|
|
31
29
|
} from "vitest";
|
|
32
|
-
import
|
|
30
|
+
import {
|
|
31
|
+
type ProcaptchaPowHandle,
|
|
32
|
+
mountProcaptchaPowWidget,
|
|
33
|
+
} from "../components/procaptchaWidget.js";
|
|
34
|
+
import { type Mounted, fire, mount, settle } from "./domHarness.js";
|
|
33
35
|
import { config, frictionless } from "./managerHarness.js";
|
|
34
36
|
|
|
35
37
|
/**
|
|
@@ -45,18 +47,6 @@ const mocks = vi.hoisted(() => {
|
|
|
45
47
|
getHoneypotValue?: () => string | undefined;
|
|
46
48
|
}[] = [];
|
|
47
49
|
const loadI18next = vi.fn<(a?: boolean, b?: string) => Promise<unknown>>();
|
|
48
|
-
const checkboxProps: {
|
|
49
|
-
current:
|
|
50
|
-
| {
|
|
51
|
-
checked: boolean;
|
|
52
|
-
loading: boolean;
|
|
53
|
-
labelText: string;
|
|
54
|
-
error?: string;
|
|
55
|
-
onChange: (event: { nativeEvent: unknown }) => Promise<void>;
|
|
56
|
-
}
|
|
57
|
-
| undefined;
|
|
58
|
-
} = { current: undefined };
|
|
59
|
-
const honeypotQuestions: string[] = [];
|
|
60
50
|
const translationsReady = { current: true };
|
|
61
51
|
return {
|
|
62
52
|
translationsReady,
|
|
@@ -64,8 +54,6 @@ const mocks = vi.hoisted(() => {
|
|
|
64
54
|
resetState,
|
|
65
55
|
constructions,
|
|
66
56
|
loadI18next,
|
|
67
|
-
checkboxProps,
|
|
68
|
-
honeypotQuestions,
|
|
69
57
|
};
|
|
70
58
|
});
|
|
71
59
|
|
|
@@ -84,56 +72,22 @@ vi.mock("../services/Manager.js", () => ({
|
|
|
84
72
|
},
|
|
85
73
|
}));
|
|
86
74
|
|
|
87
|
-
// The checkbox and the honeypot are procaptcha-common's, and tested there. The
|
|
88
|
-
// stubs keep their contract — a change callback and a forwarded input ref —
|
|
89
|
-
// while letting a test drive the exact browser event the widget branches on,
|
|
90
|
-
// which jsdom cannot produce (it marks every dispatched event untrusted, and
|
|
91
|
-
// the real checkbox drops those before the widget ever sees them).
|
|
92
|
-
vi.mock("@prosopo/procaptcha-common", async (importOriginal) => {
|
|
93
|
-
const actual =
|
|
94
|
-
await importOriginal<typeof import("@prosopo/procaptcha-common")>();
|
|
95
|
-
const { createElement, forwardRef } = await import("react");
|
|
96
|
-
interface CheckboxStubProps {
|
|
97
|
-
checked: boolean;
|
|
98
|
-
loading: boolean;
|
|
99
|
-
labelText: string;
|
|
100
|
-
error?: string;
|
|
101
|
-
onChange: (event: { nativeEvent: unknown }) => Promise<void>;
|
|
102
|
-
}
|
|
103
|
-
const Checkbox = (props: CheckboxStubProps) => {
|
|
104
|
-
mocks.checkboxProps.current = props;
|
|
105
|
-
return createElement("input", {
|
|
106
|
-
type: "checkbox",
|
|
107
|
-
readOnly: true,
|
|
108
|
-
checked: props.checked,
|
|
109
|
-
"aria-label": props.labelText,
|
|
110
|
-
"data-error": props.error,
|
|
111
|
-
"data-loading": String(props.loading),
|
|
112
|
-
});
|
|
113
|
-
};
|
|
114
|
-
const Honeypot = forwardRef<HTMLInputElement, { encodedQuestion: string }>(
|
|
115
|
-
({ encodedQuestion }, ref) => {
|
|
116
|
-
mocks.honeypotQuestions.push(encodedQuestion);
|
|
117
|
-
return createElement("input", { type: "text", ref, name: "honeypot" });
|
|
118
|
-
},
|
|
119
|
-
);
|
|
120
|
-
return { ...actual, Checkbox, Honeypot };
|
|
121
|
-
});
|
|
122
|
-
|
|
123
75
|
vi.mock("@prosopo/locale", async (importOriginal) => {
|
|
124
76
|
const actual = await importOriginal<typeof import("@prosopo/locale")>();
|
|
125
77
|
return {
|
|
126
78
|
...actual,
|
|
127
79
|
loadI18next: mocks.loadI18next,
|
|
128
|
-
|
|
80
|
+
createTranslator: () => ({
|
|
129
81
|
t: (key: string) => key,
|
|
130
|
-
|
|
82
|
+
isReady: () => mocks.translationsReady.current,
|
|
83
|
+
subscribe: () => () => undefined,
|
|
84
|
+
i18n: undefined,
|
|
131
85
|
}),
|
|
132
86
|
};
|
|
133
87
|
});
|
|
134
88
|
|
|
135
|
-
let
|
|
136
|
-
let
|
|
89
|
+
let mounted: Mounted;
|
|
90
|
+
let widget: ProcaptchaPowHandle | undefined;
|
|
137
91
|
|
|
138
92
|
const i18nStub = (
|
|
139
93
|
language: string,
|
|
@@ -148,36 +102,28 @@ const props = (overrides: Partial<ProcaptchaProps> = {}): ProcaptchaProps => ({
|
|
|
148
102
|
});
|
|
149
103
|
|
|
150
104
|
const render = (widgetProps: ProcaptchaProps): void => {
|
|
151
|
-
|
|
152
|
-
root.render(createElement(Procaptcha, widgetProps) as ReactElement);
|
|
153
|
-
});
|
|
105
|
+
widget = mountProcaptchaPowWidget(mounted.container, widgetProps);
|
|
154
106
|
};
|
|
155
107
|
|
|
156
108
|
beforeEach(() => {
|
|
157
109
|
vi.clearAllMocks();
|
|
158
110
|
mocks.constructions.length = 0;
|
|
159
|
-
mocks.honeypotQuestions.length = 0;
|
|
160
111
|
mocks.translationsReady.current = true;
|
|
161
|
-
mocks.checkboxProps.current = undefined;
|
|
162
112
|
mocks.start.mockResolvedValue(undefined);
|
|
163
113
|
mocks.loadI18next.mockResolvedValue(undefined);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
act(() => {
|
|
167
|
-
root = createRoot(container);
|
|
168
|
-
});
|
|
114
|
+
widget = undefined;
|
|
115
|
+
mounted = mount();
|
|
169
116
|
});
|
|
170
117
|
|
|
171
118
|
afterEach(() => {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
container.remove();
|
|
119
|
+
widget?.destroy();
|
|
120
|
+
widget = undefined;
|
|
121
|
+
mounted.unmount();
|
|
176
122
|
vi.restoreAllMocks();
|
|
177
123
|
});
|
|
178
124
|
|
|
179
125
|
const checkbox = (): HTMLInputElement => {
|
|
180
|
-
const element = container.querySelector<HTMLInputElement>(
|
|
126
|
+
const element = mounted.container.querySelector<HTMLInputElement>(
|
|
181
127
|
'input[type="checkbox"]',
|
|
182
128
|
);
|
|
183
129
|
if (!element) throw new Error("expected a checkbox to be rendered");
|
|
@@ -185,13 +131,16 @@ const checkbox = (): HTMLInputElement => {
|
|
|
185
131
|
};
|
|
186
132
|
|
|
187
133
|
const honeypotInput = (): HTMLInputElement => {
|
|
188
|
-
const element =
|
|
189
|
-
'input[name="
|
|
134
|
+
const element = document.querySelector<HTMLInputElement>(
|
|
135
|
+
'input[name="email_confirm"]',
|
|
190
136
|
);
|
|
191
137
|
if (!element) throw new Error("expected a honeypot to be rendered");
|
|
192
138
|
return element;
|
|
193
139
|
};
|
|
194
140
|
|
|
141
|
+
const spinner = (): Element | null =>
|
|
142
|
+
mounted.container.querySelector('[role="status"]');
|
|
143
|
+
|
|
195
144
|
interface ClickOptions {
|
|
196
145
|
trusted?: boolean;
|
|
197
146
|
clientX?: number;
|
|
@@ -199,17 +148,14 @@ interface ClickOptions {
|
|
|
199
148
|
touches?: { clientX: number; clientY: number }[];
|
|
200
149
|
}
|
|
201
150
|
|
|
202
|
-
/**
|
|
151
|
+
/** Click the real checkbox with the browser event a user would produce. */
|
|
203
152
|
const click = (options: ClickOptions = {}): void => {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
act(() => {
|
|
211
|
-
void mocks.checkboxProps.current?.onChange({ nativeEvent });
|
|
212
|
-
});
|
|
153
|
+
fire(checkbox(), "click", options);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const setState = async (next: Partial<ProcaptchaState>): Promise<void> => {
|
|
157
|
+
mocks.constructions[0]?.updateState(next);
|
|
158
|
+
await settle();
|
|
213
159
|
};
|
|
214
160
|
|
|
215
161
|
describe("what the widget renders", () => {
|
|
@@ -220,32 +166,38 @@ describe("what the widget renders", () => {
|
|
|
220
166
|
|
|
221
167
|
test("an invisible widget shows no checkbox at all", () => {
|
|
222
168
|
render(props({ config: config({ mode: ModeEnum.invisible }) }));
|
|
223
|
-
expect(
|
|
169
|
+
expect(
|
|
170
|
+
mounted.container.querySelector('input[type="checkbox"]'),
|
|
171
|
+
).toBeNull();
|
|
224
172
|
});
|
|
225
173
|
|
|
226
174
|
test("a session with a honeypot question renders the honeypot", () => {
|
|
227
|
-
render(
|
|
175
|
+
render(
|
|
176
|
+
props({ frictionlessState: frictionless({ hp: btoa("question") }) }),
|
|
177
|
+
);
|
|
228
178
|
expect(honeypotInput()).toBeDefined();
|
|
229
|
-
expect(
|
|
179
|
+
expect(document.body.textContent).toContain("question");
|
|
230
180
|
});
|
|
231
181
|
|
|
232
182
|
test("a session without one renders no bait at all", () => {
|
|
233
183
|
render(props({ frictionlessState: frictionless() }));
|
|
234
|
-
expect(
|
|
184
|
+
expect(document.querySelector('input[name="email_confirm"]')).toBeNull();
|
|
235
185
|
});
|
|
236
186
|
|
|
237
187
|
test("an invisible widget still renders the honeypot, as bait", () => {
|
|
238
188
|
render(
|
|
239
189
|
props({
|
|
240
190
|
config: config({ mode: ModeEnum.invisible }),
|
|
241
|
-
frictionlessState: frictionless({ hp: "question" }),
|
|
191
|
+
frictionlessState: frictionless({ hp: btoa("question") }),
|
|
242
192
|
}),
|
|
243
193
|
);
|
|
244
194
|
expect(honeypotInput()).toBeDefined();
|
|
245
195
|
});
|
|
246
196
|
|
|
247
197
|
test("the manager can read the honeypot input the widget rendered", () => {
|
|
248
|
-
render(
|
|
198
|
+
render(
|
|
199
|
+
props({ frictionlessState: frictionless({ hp: btoa("question") }) }),
|
|
200
|
+
);
|
|
249
201
|
honeypotInput().value = "bot@example.com";
|
|
250
202
|
expect(mocks.constructions[0]?.getHoneypotValue?.()).toBe(
|
|
251
203
|
"bot@example.com",
|
|
@@ -259,14 +211,14 @@ describe("what the widget renders", () => {
|
|
|
259
211
|
|
|
260
212
|
test("the checkbox is labelled once the translations are ready", () => {
|
|
261
213
|
render(props());
|
|
262
|
-
expect(
|
|
214
|
+
expect(checkbox().getAttribute("aria-label")).toBe("WIDGET.I_AM_HUMAN");
|
|
263
215
|
});
|
|
264
216
|
|
|
265
217
|
test("the checkbox is left unlabelled until the translations load", () => {
|
|
266
218
|
// A key like WIDGET.I_AM_HUMAN rendered raw is worse than no label.
|
|
267
219
|
mocks.translationsReady.current = false;
|
|
268
220
|
render(props());
|
|
269
|
-
expect(
|
|
221
|
+
expect(checkbox().getAttribute("aria-label")).toBe("");
|
|
270
222
|
});
|
|
271
223
|
|
|
272
224
|
test("a page that registered no callbacks still renders", () => {
|
|
@@ -280,18 +232,18 @@ describe("what the widget renders", () => {
|
|
|
280
232
|
expect(checkbox()).toBeDefined();
|
|
281
233
|
});
|
|
282
234
|
|
|
283
|
-
test("the checkbox shows the current error", () => {
|
|
235
|
+
test("the checkbox shows the current error", async () => {
|
|
284
236
|
render(props());
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
error: { message: "no session", key: "API.UNKNOWN_ERROR" },
|
|
288
|
-
});
|
|
237
|
+
await setState({
|
|
238
|
+
error: { message: "no session", key: "API.UNKNOWN_ERROR" },
|
|
289
239
|
});
|
|
290
|
-
expect(
|
|
240
|
+
expect(mounted.container.textContent).toContain("no session");
|
|
291
241
|
});
|
|
292
242
|
|
|
293
243
|
test("an unfilled honeypot reads as nothing, not as an empty answer", () => {
|
|
294
|
-
render(
|
|
244
|
+
render(
|
|
245
|
+
props({ frictionlessState: frictionless({ hp: btoa("question") }) }),
|
|
246
|
+
);
|
|
295
247
|
expect(mocks.constructions[0]?.getHoneypotValue?.()).toBeUndefined();
|
|
296
248
|
});
|
|
297
249
|
|
|
@@ -299,6 +251,15 @@ describe("what the widget renders", () => {
|
|
|
299
251
|
render(props());
|
|
300
252
|
expect(mocks.constructions[0]?.getHoneypotValue?.()).toBeUndefined();
|
|
301
253
|
});
|
|
254
|
+
|
|
255
|
+
test("the honeypot leaves the page with the widget", () => {
|
|
256
|
+
render(
|
|
257
|
+
props({ frictionlessState: frictionless({ hp: btoa("question") }) }),
|
|
258
|
+
);
|
|
259
|
+
widget?.destroy();
|
|
260
|
+
widget = undefined;
|
|
261
|
+
expect(document.querySelector('input[name="email_confirm"]')).toBeNull();
|
|
262
|
+
});
|
|
302
263
|
});
|
|
303
264
|
|
|
304
265
|
describe("starting a solve", () => {
|
|
@@ -320,21 +281,18 @@ describe("starting a solve", () => {
|
|
|
320
281
|
expect(mocks.start).toHaveBeenCalledWith(3, 4);
|
|
321
282
|
});
|
|
322
283
|
|
|
323
|
-
test("
|
|
324
|
-
//
|
|
325
|
-
//
|
|
284
|
+
test("a synthetic click never reaches the manager", () => {
|
|
285
|
+
// The checkbox drops untrusted events before the widget sees them, so an
|
|
286
|
+
// automated solver cannot start a session at all. The widget's own
|
|
287
|
+
// isTrusted guard on the coordinates is defence in depth behind it.
|
|
326
288
|
render(props());
|
|
327
289
|
click({ trusted: false, clientX: 12, clientY: 34 });
|
|
328
|
-
expect(mocks.start).
|
|
290
|
+
expect(mocks.start).not.toHaveBeenCalled();
|
|
329
291
|
});
|
|
330
292
|
|
|
331
|
-
test("
|
|
293
|
+
test("a keyboard activation is treated as the origin", () => {
|
|
332
294
|
render(props());
|
|
333
|
-
|
|
334
|
-
void mocks.checkboxProps.current?.onChange({
|
|
335
|
-
nativeEvent: { isTrusted: true },
|
|
336
|
-
});
|
|
337
|
-
});
|
|
295
|
+
fire(checkbox(), "keydown", { key: "Enter" });
|
|
338
296
|
expect(mocks.start).toHaveBeenCalledWith(0, 0);
|
|
339
297
|
});
|
|
340
298
|
|
|
@@ -342,46 +300,43 @@ describe("starting a solve", () => {
|
|
|
342
300
|
let release = (): void => undefined;
|
|
343
301
|
mocks.start.mockImplementation(
|
|
344
302
|
() =>
|
|
345
|
-
new Promise<void>((resolve) => {
|
|
303
|
+
new Promise<void>((resolve: () => void) => {
|
|
346
304
|
release = () => resolve();
|
|
347
305
|
}),
|
|
348
306
|
);
|
|
349
307
|
render(props());
|
|
350
308
|
click();
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
expect(
|
|
309
|
+
await settle();
|
|
310
|
+
expect(spinner()).not.toBeNull();
|
|
311
|
+
release();
|
|
312
|
+
await settle();
|
|
313
|
+
expect(spinner()).toBeNull();
|
|
356
314
|
});
|
|
357
315
|
|
|
358
316
|
test("a solve that rejects still clears the spinner", async () => {
|
|
359
|
-
// Nothing else would:
|
|
360
|
-
//
|
|
317
|
+
// Nothing else would: the promise is not awaited by the caller, so the
|
|
318
|
+
// rejection used to escape unhandled with the spinner still turning.
|
|
361
319
|
vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
362
320
|
mocks.start.mockRejectedValue(new Error("provider down"));
|
|
363
321
|
render(props({ autoStart: true }));
|
|
364
|
-
await
|
|
365
|
-
expect(
|
|
322
|
+
await settle();
|
|
323
|
+
expect(spinner()).toBeNull();
|
|
366
324
|
});
|
|
367
325
|
|
|
368
326
|
test("a click whose solve rejects clears the spinner too", async () => {
|
|
369
327
|
vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
370
328
|
mocks.start.mockRejectedValue(new Error("provider down"));
|
|
371
329
|
render(props());
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
});
|
|
376
|
-
});
|
|
377
|
-
expect(mocks.checkboxProps.current?.loading).toBe(false);
|
|
330
|
+
click({ clientX: 1, clientY: 2 });
|
|
331
|
+
await settle();
|
|
332
|
+
expect(spinner()).toBeNull();
|
|
378
333
|
});
|
|
379
334
|
|
|
380
335
|
test("a second click while the first solve is running is ignored", async () => {
|
|
381
336
|
let release = (): void => undefined;
|
|
382
337
|
mocks.start.mockImplementation(
|
|
383
338
|
() =>
|
|
384
|
-
new Promise<void>((resolve) => {
|
|
339
|
+
new Promise<void>((resolve: () => void) => {
|
|
385
340
|
release = () => resolve();
|
|
386
341
|
}),
|
|
387
342
|
);
|
|
@@ -389,9 +344,8 @@ describe("starting a solve", () => {
|
|
|
389
344
|
click();
|
|
390
345
|
click();
|
|
391
346
|
expect(mocks.start).toHaveBeenCalledTimes(1);
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
});
|
|
347
|
+
release();
|
|
348
|
+
await settle();
|
|
395
349
|
});
|
|
396
350
|
|
|
397
351
|
test("an autoStart widget solves without waiting for a click", () => {
|
|
@@ -412,9 +366,7 @@ describe("starting a solve", () => {
|
|
|
412
366
|
|
|
413
367
|
describe("the invisible-mode execute event", () => {
|
|
414
368
|
const execute = (): void => {
|
|
415
|
-
|
|
416
|
-
document.dispatchEvent(new Event("procaptcha:execute"));
|
|
417
|
-
});
|
|
369
|
+
document.dispatchEvent(new Event("procaptcha:execute"));
|
|
418
370
|
};
|
|
419
371
|
|
|
420
372
|
test("starts a solve", () => {
|
|
@@ -430,10 +382,8 @@ describe("the invisible-mode execute event", () => {
|
|
|
430
382
|
});
|
|
431
383
|
|
|
432
384
|
const executeOn = async (target: Element): Promise<void> => {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
436
|
-
});
|
|
385
|
+
target.dispatchEvent(new Event("procaptcha:execute"));
|
|
386
|
+
await settle();
|
|
437
387
|
};
|
|
438
388
|
|
|
439
389
|
test("a targeted execute on the container runs a visible widget", async () => {
|
|
@@ -457,10 +407,8 @@ describe("the invisible-mode execute event", () => {
|
|
|
457
407
|
|
|
458
408
|
test("stops being listened for once the widget is gone", () => {
|
|
459
409
|
render(props({ config: config({ mode: ModeEnum.invisible }) }));
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
root = createRoot(container);
|
|
463
|
-
});
|
|
410
|
+
widget?.destroy();
|
|
411
|
+
widget = undefined;
|
|
464
412
|
execute();
|
|
465
413
|
expect(mocks.start).not.toHaveBeenCalled();
|
|
466
414
|
});
|
|
@@ -481,65 +429,51 @@ describe("the invisible-mode execute event", () => {
|
|
|
481
429
|
mocks.start.mockRejectedValue(new Error("no provider"));
|
|
482
430
|
render(props({ config: config({ mode: ModeEnum.invisible }) }));
|
|
483
431
|
execute();
|
|
484
|
-
await
|
|
432
|
+
await settle();
|
|
485
433
|
expect(reported).toHaveBeenCalled();
|
|
486
434
|
});
|
|
487
435
|
});
|
|
488
436
|
|
|
489
437
|
describe("an invalidated session", () => {
|
|
490
|
-
const fail = (key: string): void => {
|
|
491
|
-
|
|
492
|
-
mocks.constructions[0]?.updateState({
|
|
493
|
-
error: { message: "boom", key },
|
|
494
|
-
});
|
|
495
|
-
});
|
|
438
|
+
const fail = async (key: string): Promise<void> => {
|
|
439
|
+
await setState({ error: { message: "boom", key } });
|
|
496
440
|
};
|
|
497
441
|
|
|
498
|
-
test("is handed to the recovery-aware parent, with the original click", () => {
|
|
442
|
+
test("is handed to the recovery-aware parent, with the original click", async () => {
|
|
499
443
|
const onSessionInvalidated = vi.fn<(x?: number, y?: number) => void>();
|
|
500
444
|
render(props({ onSessionInvalidated }));
|
|
501
445
|
click({ clientX: 9, clientY: 8 });
|
|
502
|
-
fail("CAPTCHA.NO_SESSION_FOUND");
|
|
446
|
+
await fail("CAPTCHA.NO_SESSION_FOUND");
|
|
503
447
|
expect(onSessionInvalidated).toHaveBeenCalledWith(9, 8);
|
|
504
448
|
});
|
|
505
449
|
|
|
506
|
-
test("is escalated once only, so a failing retry cannot loop", () => {
|
|
450
|
+
test("is escalated once only, so a failing retry cannot loop", async () => {
|
|
507
451
|
const onSessionInvalidated = vi.fn<(x?: number, y?: number) => void>();
|
|
508
452
|
const restart = vi.fn<() => void>();
|
|
509
|
-
vi.useFakeTimers();
|
|
510
453
|
render(
|
|
511
454
|
props({
|
|
512
455
|
onSessionInvalidated,
|
|
513
456
|
frictionlessState: frictionless({ restart }),
|
|
514
457
|
}),
|
|
515
458
|
);
|
|
516
|
-
fail("CAPTCHA.NO_SESSION_FOUND");
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
});
|
|
520
|
-
fail("CAPTCHA.NO_SESSION_FOUND");
|
|
459
|
+
await fail("CAPTCHA.NO_SESSION_FOUND");
|
|
460
|
+
await setState({ error: undefined });
|
|
461
|
+
await fail("CAPTCHA.NO_SESSION_FOUND");
|
|
521
462
|
expect(onSessionInvalidated).toHaveBeenCalledTimes(1);
|
|
522
|
-
|
|
523
|
-
vi.advanceTimersByTime(100);
|
|
524
|
-
});
|
|
463
|
+
await new Promise<void>((resolve: () => void) => setTimeout(resolve, 150));
|
|
525
464
|
expect(restart).toHaveBeenCalledTimes(1);
|
|
526
|
-
vi.useRealTimers();
|
|
527
465
|
});
|
|
528
466
|
|
|
529
|
-
test("without a recovery-aware parent the frictionless session restarts", () => {
|
|
467
|
+
test("without a recovery-aware parent the frictionless session restarts", async () => {
|
|
530
468
|
const restart = vi.fn<() => void>();
|
|
531
|
-
vi.useFakeTimers();
|
|
532
469
|
render(props({ frictionlessState: frictionless({ restart }) }));
|
|
533
|
-
fail("CAPTCHA.NO_SESSION_FOUND");
|
|
470
|
+
await fail("CAPTCHA.NO_SESSION_FOUND");
|
|
534
471
|
expect(restart).not.toHaveBeenCalled();
|
|
535
|
-
|
|
536
|
-
vi.advanceTimersByTime(100);
|
|
537
|
-
});
|
|
472
|
+
await new Promise<void>((resolve: () => void) => setTimeout(resolve, 150));
|
|
538
473
|
expect(restart).toHaveBeenCalledTimes(1);
|
|
539
|
-
vi.useRealTimers();
|
|
540
474
|
});
|
|
541
475
|
|
|
542
|
-
test("any other error is left for the widget to display", () => {
|
|
476
|
+
test("any other error is left for the widget to display", async () => {
|
|
543
477
|
const onSessionInvalidated = vi.fn<(x?: number, y?: number) => void>();
|
|
544
478
|
const restart = vi.fn<() => void>();
|
|
545
479
|
render(
|
|
@@ -548,14 +482,14 @@ describe("an invalidated session", () => {
|
|
|
548
482
|
frictionlessState: frictionless({ restart }),
|
|
549
483
|
}),
|
|
550
484
|
);
|
|
551
|
-
fail("API.UNKNOWN_ERROR");
|
|
485
|
+
await fail("API.UNKNOWN_ERROR");
|
|
552
486
|
expect(onSessionInvalidated).not.toHaveBeenCalled();
|
|
553
487
|
expect(restart).not.toHaveBeenCalled();
|
|
554
488
|
});
|
|
555
489
|
|
|
556
|
-
test("an error with nowhere to escalate to is simply shown", () => {
|
|
490
|
+
test("an error with nowhere to escalate to is simply shown", async () => {
|
|
557
491
|
render(props());
|
|
558
|
-
expect(
|
|
492
|
+
await expect(fail("CAPTCHA.NO_SESSION_FOUND")).resolves.toBeUndefined();
|
|
559
493
|
});
|
|
560
494
|
});
|
|
561
495
|
|
package/tsconfig.cjs.json
CHANGED