@prosopo/procaptcha-bundle 4.1.48 → 4.1.50
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 +26 -26
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +56 -0
- package/dist/cjs/index.cjs +79 -18
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +79 -19
- package/dist/index.js.map +1 -1
- package/dist/tests/reset.unit.test.d.ts +2 -0
- package/dist/tests/reset.unit.test.d.ts.map +1 -0
- package/dist/tests/reset.unit.test.js +120 -0
- package/dist/tests/reset.unit.test.js.map +1 -0
- package/package.json +9 -9
- package/src/index.ts +149 -34
- package/src/tests/reset.unit.test.ts +204 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vite.config.ts +12 -0
- package/vite.iife.config.ts +12 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
const mocks = vi.hoisted(() => ({
|
|
3
|
+
prefetchDetector: vi.fn(),
|
|
4
|
+
createWidgets: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
vi.mock("@prosopo/procaptcha-frictionless", () => ({
|
|
7
|
+
prefetchDetector: mocks.prefetchDetector,
|
|
8
|
+
}));
|
|
9
|
+
vi.mock("@prosopo/procaptcha-common", () => ({
|
|
10
|
+
getWindowCallback: vi.fn(),
|
|
11
|
+
pickIpMode: vi.fn(() => undefined),
|
|
12
|
+
}));
|
|
13
|
+
vi.mock("../util/widgetFactory.js", () => ({
|
|
14
|
+
WidgetFactory: vi.fn(function () {
|
|
15
|
+
return { createWidgets: mocks.createWidgets };
|
|
16
|
+
}),
|
|
17
|
+
}));
|
|
18
|
+
const { render, reset, remove } = await import("../index.js");
|
|
19
|
+
const SITE_KEY = "5CcNvLUdiXFpzKDMjThGLSK9rhWHA1H4EF3zrgkpkjAdqmuP";
|
|
20
|
+
const makeRoot = () => ({ unmount: vi.fn(), render: vi.fn() });
|
|
21
|
+
const queueRoots = (...roots) => {
|
|
22
|
+
for (const root of roots) {
|
|
23
|
+
mocks.createWidgets.mockResolvedValueOnce([root]);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
vi.clearAllMocks();
|
|
28
|
+
await remove();
|
|
29
|
+
mocks.createWidgets.mockResolvedValue([makeRoot()]);
|
|
30
|
+
});
|
|
31
|
+
describe("render", () => {
|
|
32
|
+
it("returns a widget id", async () => {
|
|
33
|
+
queueRoots(makeRoot());
|
|
34
|
+
const widgetId = await render(document.createElement("div"), {
|
|
35
|
+
siteKey: SITE_KEY,
|
|
36
|
+
});
|
|
37
|
+
expect(typeof widgetId).toBe("string");
|
|
38
|
+
expect(widgetId).toBeTruthy();
|
|
39
|
+
});
|
|
40
|
+
it("returns undefined when the factory creates no widget", async () => {
|
|
41
|
+
mocks.createWidgets.mockResolvedValueOnce([]);
|
|
42
|
+
const widgetId = await render(document.createElement("div"), {
|
|
43
|
+
siteKey: SITE_KEY,
|
|
44
|
+
});
|
|
45
|
+
expect(widgetId).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe("reset", () => {
|
|
49
|
+
it("unmounts the old root and mounts a replacement", async () => {
|
|
50
|
+
const first = makeRoot();
|
|
51
|
+
const second = makeRoot();
|
|
52
|
+
queueRoots(first, second);
|
|
53
|
+
const element = document.createElement("div");
|
|
54
|
+
await render(element, { siteKey: SITE_KEY });
|
|
55
|
+
expect(mocks.createWidgets).toHaveBeenCalledTimes(1);
|
|
56
|
+
await reset();
|
|
57
|
+
expect(first.unmount).toHaveBeenCalledTimes(1);
|
|
58
|
+
expect(mocks.createWidgets).toHaveBeenCalledTimes(2);
|
|
59
|
+
expect(mocks.createWidgets).toHaveBeenLastCalledWith([element], expect.objectContaining({ siteKey: SITE_KEY }), true, false);
|
|
60
|
+
expect(second.unmount).not.toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
it("resets only the widget whose id is given", async () => {
|
|
63
|
+
const firstA = makeRoot();
|
|
64
|
+
const firstB = makeRoot();
|
|
65
|
+
const replacementA = makeRoot();
|
|
66
|
+
queueRoots(firstA, firstB, replacementA);
|
|
67
|
+
const elementA = document.createElement("div");
|
|
68
|
+
const elementB = document.createElement("div");
|
|
69
|
+
const idA = await render(elementA, { siteKey: SITE_KEY });
|
|
70
|
+
await render(elementB, { siteKey: SITE_KEY });
|
|
71
|
+
await reset(idA);
|
|
72
|
+
expect(firstA.unmount).toHaveBeenCalledTimes(1);
|
|
73
|
+
expect(firstB.unmount).not.toHaveBeenCalled();
|
|
74
|
+
expect(mocks.createWidgets).toHaveBeenLastCalledWith([elementA], expect.anything(), true, false);
|
|
75
|
+
});
|
|
76
|
+
it("keeps the widget resettable more than once", async () => {
|
|
77
|
+
const roots = [makeRoot(), makeRoot(), makeRoot()];
|
|
78
|
+
queueRoots(...roots);
|
|
79
|
+
await render(document.createElement("div"), { siteKey: SITE_KEY });
|
|
80
|
+
await reset();
|
|
81
|
+
await reset();
|
|
82
|
+
expect(roots[0]?.unmount).toHaveBeenCalledTimes(1);
|
|
83
|
+
expect(roots[1]?.unmount).toHaveBeenCalledTimes(1);
|
|
84
|
+
expect(mocks.createWidgets).toHaveBeenCalledTimes(3);
|
|
85
|
+
});
|
|
86
|
+
it("preserves the invisible flag when rebuilding", async () => {
|
|
87
|
+
queueRoots(makeRoot(), makeRoot());
|
|
88
|
+
const button = document.createElement("button");
|
|
89
|
+
await render(button, { siteKey: SITE_KEY });
|
|
90
|
+
await reset();
|
|
91
|
+
expect(mocks.createWidgets).toHaveBeenLastCalledWith([button], expect.anything(), true, true);
|
|
92
|
+
});
|
|
93
|
+
it("does nothing for an unknown widget id", async () => {
|
|
94
|
+
queueRoots(makeRoot());
|
|
95
|
+
await render(document.createElement("div"), { siteKey: SITE_KEY });
|
|
96
|
+
await reset("procaptcha-widget-does-not-exist");
|
|
97
|
+
expect(mocks.createWidgets).toHaveBeenCalledTimes(1);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
describe("remove", () => {
|
|
101
|
+
it("unmounts without mounting a replacement", async () => {
|
|
102
|
+
const root = makeRoot();
|
|
103
|
+
queueRoots(root);
|
|
104
|
+
const element = document.createElement("div");
|
|
105
|
+
element.innerHTML = "<span>skeleton</span>";
|
|
106
|
+
await render(element, { siteKey: SITE_KEY });
|
|
107
|
+
await remove();
|
|
108
|
+
expect(root.unmount).toHaveBeenCalledTimes(1);
|
|
109
|
+
expect(mocks.createWidgets).toHaveBeenCalledTimes(1);
|
|
110
|
+
expect(element.innerHTML).toBe("");
|
|
111
|
+
});
|
|
112
|
+
it("makes a subsequent reset a no-op", async () => {
|
|
113
|
+
queueRoots(makeRoot());
|
|
114
|
+
await render(document.createElement("div"), { siteKey: SITE_KEY });
|
|
115
|
+
await remove();
|
|
116
|
+
await reset();
|
|
117
|
+
expect(mocks.createWidgets).toHaveBeenCalledTimes(1);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
//# sourceMappingURL=reset.unit.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reset.unit.test.js","sourceRoot":"","sources":["../../src/tests/reset.unit.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prosopo/procaptcha-bundle",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.50",
|
|
4
4
|
"author": "PROSOPO LIMITED <info@prosopo.io>",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -39,13 +39,13 @@
|
|
|
39
39
|
},
|
|
40
40
|
"browserslist": ["> 0.5%, last 2 versions, not dead"],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@prosopo/dotenv": "3.0.
|
|
43
|
-
"@prosopo/locale": "3.
|
|
44
|
-
"@prosopo/procaptcha-common": "2.11.
|
|
45
|
-
"@prosopo/procaptcha-frictionless": "2.13.
|
|
46
|
-
"@prosopo/types": "5.0.
|
|
47
|
-
"@prosopo/util": "3.3.
|
|
48
|
-
"@prosopo/widget-skeleton": "2.8.
|
|
42
|
+
"@prosopo/dotenv": "3.0.52",
|
|
43
|
+
"@prosopo/locale": "3.3.0",
|
|
44
|
+
"@prosopo/procaptcha-common": "2.11.24",
|
|
45
|
+
"@prosopo/procaptcha-frictionless": "2.13.6",
|
|
46
|
+
"@prosopo/types": "5.0.4",
|
|
47
|
+
"@prosopo/util": "3.3.6",
|
|
48
|
+
"@prosopo/widget-skeleton": "2.8.6",
|
|
49
49
|
"dotenv": "16.4.5",
|
|
50
50
|
"fast-glob": "3.3.2",
|
|
51
51
|
"jsdom": "25.0.0",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"registry": "https://registry.npmjs.org"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
-
"@prosopo/config": "3.3.
|
|
71
|
+
"@prosopo/config": "3.3.8",
|
|
72
72
|
"@types/jsdom": "21.1.7",
|
|
73
73
|
"@types/node": "22.10.2",
|
|
74
74
|
"@types/react": "18.3.1",
|
package/src/index.ts
CHANGED
|
@@ -21,7 +21,49 @@ import { WidgetFactory } from "./util/widgetFactory.js";
|
|
|
21
21
|
import { WidgetThemeResolver } from "./util/widgetThemeResolver.js";
|
|
22
22
|
|
|
23
23
|
const BUNDLE_NAMES = ["procaptcha.bundle.iife.js", "procaptcha.bundle.js"];
|
|
24
|
-
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Everything needed to rebuild a widget in place.
|
|
27
|
+
*
|
|
28
|
+
* Previously only the React `Root` was retained, which made `reset()` a
|
|
29
|
+
* one-way operation: unmounting tore out the React tree but the widget
|
|
30
|
+
* skeleton (created imperatively by `createWidgetSkeleton`, not by React)
|
|
31
|
+
* stayed in the DOM, leaving a container with a logo and no checkbox. There
|
|
32
|
+
* was no record of which element or render options produced it, so nothing
|
|
33
|
+
* could put it back. Keeping the descriptor alongside the root is what lets
|
|
34
|
+
* `reset()` remount rather than just destroy.
|
|
35
|
+
*/
|
|
36
|
+
interface WidgetEntry {
|
|
37
|
+
root: Root;
|
|
38
|
+
element: Element;
|
|
39
|
+
renderOptions: ProcaptchaRenderOptions;
|
|
40
|
+
isWeb2: boolean;
|
|
41
|
+
invisible: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const procaptchaWidgets = new Map<string, WidgetEntry>();
|
|
45
|
+
|
|
46
|
+
let widgetIdCounter = 0;
|
|
47
|
+
const nextWidgetId = (): string => `procaptcha-widget-${widgetIdCounter++}`;
|
|
48
|
+
|
|
49
|
+
const registerWidgets = (
|
|
50
|
+
roots: Root[],
|
|
51
|
+
elements: Element[],
|
|
52
|
+
renderOptions: ProcaptchaRenderOptions,
|
|
53
|
+
isWeb2: boolean,
|
|
54
|
+
invisible: boolean,
|
|
55
|
+
): string[] =>
|
|
56
|
+
roots.map((root, index) => {
|
|
57
|
+
const id = nextWidgetId();
|
|
58
|
+
procaptchaWidgets.set(id, {
|
|
59
|
+
root,
|
|
60
|
+
element: at(elements, index),
|
|
61
|
+
renderOptions,
|
|
62
|
+
isWeb2,
|
|
63
|
+
invisible,
|
|
64
|
+
});
|
|
65
|
+
return id;
|
|
66
|
+
});
|
|
25
67
|
|
|
26
68
|
const widgetFactory = new WidgetFactory(new WidgetThemeResolver());
|
|
27
69
|
|
|
@@ -87,17 +129,25 @@ const implicitRender = async () => {
|
|
|
87
129
|
// `customDetectBot` claims the in-flight promise when it eventually runs.
|
|
88
130
|
startDetectorPrefetch(siteKey, { ipv4, ipv6 });
|
|
89
131
|
|
|
132
|
+
const implicitRenderOptions: ProcaptchaRenderOptions = {
|
|
133
|
+
siteKey: siteKey,
|
|
134
|
+
ipv4,
|
|
135
|
+
ipv6,
|
|
136
|
+
};
|
|
137
|
+
|
|
90
138
|
const root = await widgetFactory.createWidgets(
|
|
91
139
|
elements,
|
|
92
|
-
|
|
93
|
-
siteKey: siteKey,
|
|
94
|
-
ipv4,
|
|
95
|
-
ipv6,
|
|
96
|
-
},
|
|
140
|
+
implicitRenderOptions,
|
|
97
141
|
!(web3 === "true"),
|
|
98
142
|
);
|
|
99
143
|
|
|
100
|
-
|
|
144
|
+
registerWidgets(
|
|
145
|
+
root,
|
|
146
|
+
elements,
|
|
147
|
+
implicitRenderOptions,
|
|
148
|
+
!(web3 === "true"),
|
|
149
|
+
false,
|
|
150
|
+
);
|
|
101
151
|
}
|
|
102
152
|
|
|
103
153
|
// Check for invisible mode indicators (procaptcha class on buttons)
|
|
@@ -114,19 +164,21 @@ const implicitRender = async () => {
|
|
|
114
164
|
|
|
115
165
|
startDetectorPrefetch(siteKey, { ipv4, ipv6 });
|
|
116
166
|
|
|
167
|
+
const buttonRenderOptions: ProcaptchaRenderOptions = {
|
|
168
|
+
siteKey: siteKey,
|
|
169
|
+
callback: callback,
|
|
170
|
+
ipv4,
|
|
171
|
+
ipv6,
|
|
172
|
+
};
|
|
173
|
+
|
|
117
174
|
const root = await widgetFactory.createWidgets(
|
|
118
175
|
[button],
|
|
119
|
-
|
|
120
|
-
siteKey: siteKey,
|
|
121
|
-
callback: callback,
|
|
122
|
-
ipv4,
|
|
123
|
-
ipv6,
|
|
124
|
-
},
|
|
176
|
+
buttonRenderOptions,
|
|
125
177
|
true,
|
|
126
178
|
true,
|
|
127
179
|
);
|
|
128
180
|
|
|
129
|
-
|
|
181
|
+
registerWidgets(root, [button], buttonRenderOptions, true, true);
|
|
130
182
|
|
|
131
183
|
// Add click event listener to the button
|
|
132
184
|
button.addEventListener("click", async (event) => {
|
|
@@ -138,10 +190,19 @@ const implicitRender = async () => {
|
|
|
138
190
|
};
|
|
139
191
|
|
|
140
192
|
// Explicit render for targeting specific elements
|
|
193
|
+
/**
|
|
194
|
+
* Renders a widget into `element` and returns its id, which `reset()` and
|
|
195
|
+
* `remove()` accept to target this widget alone. Previously this returned
|
|
196
|
+
* nothing, so callers had no handle on an individual widget and the only
|
|
197
|
+
* available reset was all-or-nothing.
|
|
198
|
+
*
|
|
199
|
+
* Returns undefined when no widget was created, rather than throwing — the
|
|
200
|
+
* factory legitimately yields nothing for an element it cannot mount into.
|
|
201
|
+
*/
|
|
141
202
|
export const render = async (
|
|
142
203
|
element: Element,
|
|
143
204
|
renderOptions: ProcaptchaRenderOptions,
|
|
144
|
-
) => {
|
|
205
|
+
): Promise<string | undefined> => {
|
|
145
206
|
startDetectorPrefetch(renderOptions.siteKey, renderOptions);
|
|
146
207
|
|
|
147
208
|
const hasInvisibleSize =
|
|
@@ -149,26 +210,27 @@ export const render = async (
|
|
|
149
210
|
renderOptions.size === "invisible";
|
|
150
211
|
|
|
151
212
|
const isWeb2 = !renderOptions.web3;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const roots = await widgetFactory.createWidgets(
|
|
155
|
-
[element],
|
|
156
|
-
renderOptions,
|
|
157
|
-
isWeb2,
|
|
158
|
-
true,
|
|
159
|
-
);
|
|
160
|
-
procaptchaRoots.push(...roots);
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
213
|
+
const invisible =
|
|
214
|
+
hasInvisibleSize || element.tagName.toLowerCase() === "button";
|
|
163
215
|
|
|
164
216
|
const roots = await widgetFactory.createWidgets(
|
|
165
217
|
[element],
|
|
166
218
|
renderOptions,
|
|
167
219
|
isWeb2,
|
|
168
|
-
|
|
220
|
+
invisible,
|
|
169
221
|
);
|
|
170
222
|
|
|
171
|
-
|
|
223
|
+
const ids = registerWidgets(
|
|
224
|
+
roots,
|
|
225
|
+
[element],
|
|
226
|
+
renderOptions,
|
|
227
|
+
isWeb2,
|
|
228
|
+
invisible,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// Deliberately not `at()`: it throws on an empty array before it consults
|
|
232
|
+
// `optional`, and zero roots is a legitimate outcome here.
|
|
233
|
+
return ids[0];
|
|
172
234
|
};
|
|
173
235
|
|
|
174
236
|
export default function ready(fn: () => void) {
|
|
@@ -246,6 +308,7 @@ declare global {
|
|
|
246
308
|
ready: typeof ready;
|
|
247
309
|
render: typeof render;
|
|
248
310
|
reset: typeof reset;
|
|
311
|
+
remove: typeof remove;
|
|
249
312
|
execute: typeof execute;
|
|
250
313
|
};
|
|
251
314
|
}
|
|
@@ -284,17 +347,69 @@ const start = () => {
|
|
|
284
347
|
}
|
|
285
348
|
};
|
|
286
349
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
350
|
+
/**
|
|
351
|
+
* Returns a widget to its unsolved state, ready to be solved again. Pass a
|
|
352
|
+
* widget id to reset one widget, or omit it to reset every widget on the page.
|
|
353
|
+
*
|
|
354
|
+
* This remounts rather than merely unmounting. The previous implementation
|
|
355
|
+
* unmounted every root and then called `start()`, which only re-renders when
|
|
356
|
+
* the page uses implicit rendering — and even then only via the
|
|
357
|
+
* `document.readyState` fallback, because the script's `load` event has long
|
|
358
|
+
* since fired. On an explicitly-rendered page nothing came back at all: the
|
|
359
|
+
* skeleton stayed in the DOM with no checkbox inside it, and no fresh captcha
|
|
360
|
+
* request was ever made. Rebuilding from the stored descriptor makes reset
|
|
361
|
+
* behave the same way on both paths.
|
|
362
|
+
*
|
|
363
|
+
* `start()` is deliberately not called here any more. It re-renders implicit
|
|
364
|
+
* widgets, which would double up with the remount below, and it attaches
|
|
365
|
+
* another `load` listener to the script tag on every call.
|
|
366
|
+
*/
|
|
367
|
+
export const reset = async (widgetId?: string): Promise<void> => {
|
|
368
|
+
const ids: string[] =
|
|
369
|
+
undefined === widgetId ? Array.from(procaptchaWidgets.keys()) : [widgetId];
|
|
370
|
+
|
|
371
|
+
for (const id of ids) {
|
|
372
|
+
const current = procaptchaWidgets.get(id);
|
|
373
|
+
if (!current) continue;
|
|
374
|
+
|
|
375
|
+
current.root.unmount();
|
|
376
|
+
|
|
377
|
+
const [root] = await widgetFactory.createWidgets(
|
|
378
|
+
[current.element],
|
|
379
|
+
current.renderOptions,
|
|
380
|
+
current.isWeb2,
|
|
381
|
+
current.invisible,
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
if (root) {
|
|
385
|
+
procaptchaWidgets.set(id, { ...current, root });
|
|
386
|
+
} else {
|
|
387
|
+
procaptchaWidgets.delete(id);
|
|
388
|
+
}
|
|
290
389
|
}
|
|
291
|
-
|
|
390
|
+
};
|
|
292
391
|
|
|
293
|
-
|
|
392
|
+
/**
|
|
393
|
+
* Tears a widget down without putting it back — the behaviour `reset()` used
|
|
394
|
+
* to have by accident on explicitly-rendered pages, kept as an explicit
|
|
395
|
+
* operation for callers that genuinely want the widget gone (a closing modal,
|
|
396
|
+
* an SPA route change).
|
|
397
|
+
*/
|
|
398
|
+
export const remove = (widgetId?: string): void => {
|
|
399
|
+
const ids =
|
|
400
|
+
undefined === widgetId ? Array.from(procaptchaWidgets.keys()) : [widgetId];
|
|
401
|
+
|
|
402
|
+
for (const id of ids) {
|
|
403
|
+
const entry = procaptchaWidgets.get(id);
|
|
404
|
+
if (!entry) continue;
|
|
405
|
+
entry.root.unmount();
|
|
406
|
+
entry.element.innerHTML = "";
|
|
407
|
+
procaptchaWidgets.delete(id);
|
|
408
|
+
}
|
|
294
409
|
};
|
|
295
410
|
|
|
296
411
|
// set the procaptcha attribute on the window
|
|
297
|
-
window.procaptcha = { ready, render, reset, execute };
|
|
412
|
+
window.procaptcha = { ready, render, reset, remove, execute };
|
|
298
413
|
|
|
299
414
|
// Dispatch a custom event to notify that window.procaptcha is ready
|
|
300
415
|
const procaptchaReadyEvent = new CustomEvent(PROCAPTCHA_READY_EVENT, {
|
|
@@ -0,0 +1,204 @@
|
|
|
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
|
+
* `reset()` used to unmount every React root and then call `start()`, which
|
|
17
|
+
* re-renders only on implicitly-rendered pages. An explicitly-rendered widget
|
|
18
|
+
* was therefore destroyed and never rebuilt: the skeleton stayed in the DOM
|
|
19
|
+
* with no checkbox inside it and no fresh captcha request was made. These
|
|
20
|
+
* tests pin the remount so that regression cannot return.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { Root } from "react-dom/client";
|
|
24
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
25
|
+
|
|
26
|
+
const mocks = vi.hoisted(() => ({
|
|
27
|
+
prefetchDetector: vi.fn(),
|
|
28
|
+
createWidgets: vi.fn(),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock("@prosopo/procaptcha-frictionless", () => ({
|
|
32
|
+
prefetchDetector: mocks.prefetchDetector,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
vi.mock("@prosopo/procaptcha-common", () => ({
|
|
36
|
+
getWindowCallback: vi.fn(),
|
|
37
|
+
pickIpMode: vi.fn(() => undefined),
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
vi.mock("../util/widgetFactory.js", () => ({
|
|
41
|
+
WidgetFactory: vi.fn(function () {
|
|
42
|
+
return { createWidgets: mocks.createWidgets };
|
|
43
|
+
}),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
const { render, reset, remove } = await import("../index.js");
|
|
47
|
+
|
|
48
|
+
const SITE_KEY = "5CcNvLUdiXFpzKDMjThGLSK9rhWHA1H4EF3zrgkpkjAdqmuP";
|
|
49
|
+
|
|
50
|
+
const makeRoot = (): Root =>
|
|
51
|
+
({ unmount: vi.fn(), render: vi.fn() }) as unknown as Root;
|
|
52
|
+
|
|
53
|
+
/** Each createWidgets call yields a distinct root, as the real factory does. */
|
|
54
|
+
const queueRoots = (...roots: Root[]): void => {
|
|
55
|
+
for (const root of roots) {
|
|
56
|
+
mocks.createWidgets.mockResolvedValueOnce([root]);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
beforeEach(async () => {
|
|
61
|
+
vi.clearAllMocks();
|
|
62
|
+
// Drop any widgets registered by a previous test — module state persists
|
|
63
|
+
// across tests in the same file.
|
|
64
|
+
await remove();
|
|
65
|
+
mocks.createWidgets.mockResolvedValue([makeRoot()]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("render", () => {
|
|
69
|
+
it("returns a widget id", async () => {
|
|
70
|
+
queueRoots(makeRoot());
|
|
71
|
+
|
|
72
|
+
const widgetId = await render(document.createElement("div"), {
|
|
73
|
+
siteKey: SITE_KEY,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(typeof widgetId).toBe("string");
|
|
77
|
+
expect(widgetId).toBeTruthy();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("returns undefined when the factory creates no widget", async () => {
|
|
81
|
+
mocks.createWidgets.mockResolvedValueOnce([] as Root[]);
|
|
82
|
+
|
|
83
|
+
const widgetId = await render(document.createElement("div"), {
|
|
84
|
+
siteKey: SITE_KEY,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(widgetId).toBeUndefined();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("reset", () => {
|
|
92
|
+
it("unmounts the old root and mounts a replacement", async () => {
|
|
93
|
+
const first = makeRoot();
|
|
94
|
+
const second = makeRoot();
|
|
95
|
+
queueRoots(first, second);
|
|
96
|
+
|
|
97
|
+
const element = document.createElement("div");
|
|
98
|
+
await render(element, { siteKey: SITE_KEY });
|
|
99
|
+
expect(mocks.createWidgets).toHaveBeenCalledTimes(1);
|
|
100
|
+
|
|
101
|
+
await reset();
|
|
102
|
+
|
|
103
|
+
expect(first.unmount).toHaveBeenCalledTimes(1);
|
|
104
|
+
expect(mocks.createWidgets).toHaveBeenCalledTimes(2);
|
|
105
|
+
// Rebuilt into the same element, with the options it was rendered with.
|
|
106
|
+
expect(mocks.createWidgets).toHaveBeenLastCalledWith(
|
|
107
|
+
[element],
|
|
108
|
+
expect.objectContaining({ siteKey: SITE_KEY }),
|
|
109
|
+
true,
|
|
110
|
+
false,
|
|
111
|
+
);
|
|
112
|
+
expect(second.unmount).not.toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("resets only the widget whose id is given", async () => {
|
|
116
|
+
const firstA = makeRoot();
|
|
117
|
+
const firstB = makeRoot();
|
|
118
|
+
const replacementA = makeRoot();
|
|
119
|
+
queueRoots(firstA, firstB, replacementA);
|
|
120
|
+
|
|
121
|
+
const elementA = document.createElement("div");
|
|
122
|
+
const elementB = document.createElement("div");
|
|
123
|
+
const idA = await render(elementA, { siteKey: SITE_KEY });
|
|
124
|
+
await render(elementB, { siteKey: SITE_KEY });
|
|
125
|
+
|
|
126
|
+
await reset(idA);
|
|
127
|
+
|
|
128
|
+
expect(firstA.unmount).toHaveBeenCalledTimes(1);
|
|
129
|
+
expect(firstB.unmount).not.toHaveBeenCalled();
|
|
130
|
+
expect(mocks.createWidgets).toHaveBeenLastCalledWith(
|
|
131
|
+
[elementA],
|
|
132
|
+
expect.anything(),
|
|
133
|
+
true,
|
|
134
|
+
false,
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("keeps the widget resettable more than once", async () => {
|
|
139
|
+
const roots = [makeRoot(), makeRoot(), makeRoot()];
|
|
140
|
+
queueRoots(...roots);
|
|
141
|
+
|
|
142
|
+
await render(document.createElement("div"), { siteKey: SITE_KEY });
|
|
143
|
+
await reset();
|
|
144
|
+
await reset();
|
|
145
|
+
|
|
146
|
+
expect(roots[0]?.unmount).toHaveBeenCalledTimes(1);
|
|
147
|
+
expect(roots[1]?.unmount).toHaveBeenCalledTimes(1);
|
|
148
|
+
expect(mocks.createWidgets).toHaveBeenCalledTimes(3);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("preserves the invisible flag when rebuilding", async () => {
|
|
152
|
+
queueRoots(makeRoot(), makeRoot());
|
|
153
|
+
|
|
154
|
+
const button = document.createElement("button");
|
|
155
|
+
await render(button, { siteKey: SITE_KEY });
|
|
156
|
+
|
|
157
|
+
await reset();
|
|
158
|
+
|
|
159
|
+
expect(mocks.createWidgets).toHaveBeenLastCalledWith(
|
|
160
|
+
[button],
|
|
161
|
+
expect.anything(),
|
|
162
|
+
true,
|
|
163
|
+
true,
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("does nothing for an unknown widget id", async () => {
|
|
168
|
+
queueRoots(makeRoot());
|
|
169
|
+
await render(document.createElement("div"), { siteKey: SITE_KEY });
|
|
170
|
+
|
|
171
|
+
await reset("procaptcha-widget-does-not-exist");
|
|
172
|
+
|
|
173
|
+
expect(mocks.createWidgets).toHaveBeenCalledTimes(1);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("remove", () => {
|
|
178
|
+
it("unmounts without mounting a replacement", async () => {
|
|
179
|
+
const root = makeRoot();
|
|
180
|
+
queueRoots(root);
|
|
181
|
+
|
|
182
|
+
const element = document.createElement("div");
|
|
183
|
+
element.innerHTML = "<span>skeleton</span>";
|
|
184
|
+
await render(element, { siteKey: SITE_KEY });
|
|
185
|
+
|
|
186
|
+
await remove();
|
|
187
|
+
|
|
188
|
+
expect(root.unmount).toHaveBeenCalledTimes(1);
|
|
189
|
+
expect(mocks.createWidgets).toHaveBeenCalledTimes(1);
|
|
190
|
+
// The skeleton is plain DOM, so unmounting React alone would leave it
|
|
191
|
+
// behind — remove() is responsible for clearing the container too.
|
|
192
|
+
expect(element.innerHTML).toBe("");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("makes a subsequent reset a no-op", async () => {
|
|
196
|
+
queueRoots(makeRoot());
|
|
197
|
+
await render(document.createElement("div"), { siteKey: SITE_KEY });
|
|
198
|
+
|
|
199
|
+
await remove();
|
|
200
|
+
await reset();
|
|
201
|
+
|
|
202
|
+
expect(mocks.createWidgets).toHaveBeenCalledTimes(1);
|
|
203
|
+
});
|
|
204
|
+
});
|