@prosopo/procaptcha-bundle 4.2.5 → 4.4.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 +16 -15
- package/.turbo/turbo-build$colon$tsc.log +26 -26
- package/.turbo/turbo-build.log +16 -15
- package/CHANGELOG.md +85 -0
- package/dist/cjs/index.cjs +101 -21
- package/dist/cjs/util/captcha/captchaRenderer.cjs +20 -1
- package/dist/cjs/util/configCreator.cjs +4 -2
- package/dist/cjs/util/startMode.cjs +20 -0
- package/dist/cjs/util/widgetFactory.cjs +8 -5
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +101 -22
- package/dist/index.js.map +1 -1
- package/dist/tests/bind.unit.test.d.ts +2 -0
- package/dist/tests/bind.unit.test.d.ts.map +1 -0
- package/dist/tests/bind.unit.test.js +166 -0
- package/dist/tests/bind.unit.test.js.map +1 -0
- package/dist/tests/detectorPrefetch.unit.test.js.map +1 -1
- package/dist/tests/procaptchaReady.unit.test.js +1 -0
- package/dist/tests/procaptchaReady.unit.test.js.map +1 -1
- package/dist/tests/reset.unit.test.js +6 -2
- package/dist/tests/reset.unit.test.js.map +1 -1
- package/dist/tests/start.unit.test.d.ts +2 -0
- package/dist/tests/start.unit.test.d.ts.map +1 -0
- package/dist/tests/start.unit.test.js +103 -0
- package/dist/tests/start.unit.test.js.map +1 -0
- package/dist/tests/util/startMode.test.d.ts +2 -0
- package/dist/tests/util/startMode.test.d.ts.map +1 -0
- package/dist/tests/util/startMode.test.js +48 -0
- package/dist/tests/util/startMode.test.js.map +1 -0
- package/dist/util/captcha/captchaRenderer.d.ts +1 -1
- package/dist/util/captcha/captchaRenderer.d.ts.map +1 -1
- package/dist/util/captcha/captchaRenderer.js +20 -1
- package/dist/util/captcha/captchaRenderer.js.map +1 -1
- package/dist/util/configCreator.d.ts +14 -2
- package/dist/util/configCreator.d.ts.map +1 -1
- package/dist/util/configCreator.js +5 -3
- package/dist/util/configCreator.js.map +1 -1
- package/dist/util/startMode.d.ts +5 -0
- package/dist/util/startMode.d.ts.map +1 -0
- package/dist/util/startMode.js +18 -0
- package/dist/util/startMode.js.map +1 -0
- package/dist/util/widgetFactory.d.ts +7 -2
- package/dist/util/widgetFactory.d.ts.map +1 -1
- package/dist/util/widgetFactory.js +8 -5
- package/dist/util/widgetFactory.js.map +1 -1
- package/package.json +12 -12
- package/src/index.ts +145 -28
- package/src/tests/bind.unit.test.ts +227 -0
- package/src/tests/detectorPrefetch.unit.test.ts +2 -2
- package/src/tests/procaptchaReady.unit.test.ts +1 -0
- package/src/tests/reset.unit.test.ts +9 -3
- package/src/tests/start.unit.test.ts +155 -0
- package/src/tests/util/startMode.test.ts +113 -0
- package/src/util/captcha/captchaRenderer.tsx +33 -13
- package/src/util/configCreator.ts +29 -11
- package/src/util/startMode.ts +53 -0
- package/src/util/widgetFactory.ts +10 -3
- package/tsconfig.tsbuildinfo +1 -1
package/src/index.ts
CHANGED
|
@@ -13,11 +13,18 @@
|
|
|
13
13
|
// limitations under the License.
|
|
14
14
|
|
|
15
15
|
import { getWindowCallback } from "@prosopo/procaptcha-common";
|
|
16
|
-
import
|
|
16
|
+
import {
|
|
17
|
+
type EnvironmentTypes,
|
|
18
|
+
PROCAPTCHA_START_EVENT,
|
|
19
|
+
type ProcaptchaRenderOptions,
|
|
20
|
+
type ProcaptchaStartEventDetail,
|
|
21
|
+
StartModeEnum,
|
|
22
|
+
} from "@prosopo/types";
|
|
17
23
|
import { at } from "@prosopo/util";
|
|
18
24
|
import type { Root } from "react-dom/client";
|
|
19
25
|
import { extractParams, getProcaptchaScript } from "./util/config.js";
|
|
20
|
-
import {
|
|
26
|
+
import { resolveStartMode } from "./util/startMode.js";
|
|
27
|
+
import { type CreatedWidget, WidgetFactory } from "./util/widgetFactory.js";
|
|
21
28
|
import { WidgetThemeResolver } from "./util/widgetThemeResolver.js";
|
|
22
29
|
|
|
23
30
|
const BUNDLE_NAMES = ["procaptcha.bundle.iife.js", "procaptcha.bundle.js"];
|
|
@@ -36,9 +43,12 @@ const BUNDLE_NAMES = ["procaptcha.bundle.iife.js", "procaptcha.bundle.js"];
|
|
|
36
43
|
interface WidgetEntry {
|
|
37
44
|
root: Root;
|
|
38
45
|
element: Element;
|
|
46
|
+
/** The element the widget listens on; a targeted execute() is dispatched here. */
|
|
47
|
+
target: HTMLElement;
|
|
39
48
|
renderOptions: ProcaptchaRenderOptions;
|
|
40
49
|
isWeb2: boolean;
|
|
41
50
|
invisible: boolean;
|
|
51
|
+
unbindTrigger?: () => void;
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
const procaptchaWidgets = new Map<string, WidgetEntry>();
|
|
@@ -47,17 +57,18 @@ let widgetIdCounter = 0;
|
|
|
47
57
|
const nextWidgetId = (): string => `procaptcha-widget-${widgetIdCounter++}`;
|
|
48
58
|
|
|
49
59
|
const registerWidgets = (
|
|
50
|
-
|
|
60
|
+
widgets: CreatedWidget[],
|
|
51
61
|
elements: Element[],
|
|
52
62
|
renderOptions: ProcaptchaRenderOptions,
|
|
53
63
|
isWeb2: boolean,
|
|
54
64
|
invisible: boolean,
|
|
55
65
|
): string[] =>
|
|
56
|
-
|
|
66
|
+
widgets.map(({ root, container }, index) => {
|
|
57
67
|
const id = nextWidgetId();
|
|
58
68
|
procaptchaWidgets.set(id, {
|
|
59
69
|
root,
|
|
60
70
|
element: at(elements, index),
|
|
71
|
+
target: container,
|
|
61
72
|
renderOptions,
|
|
62
73
|
isWeb2,
|
|
63
74
|
invisible,
|
|
@@ -80,9 +91,10 @@ const widgetFactory = new WidgetFactory(new WidgetThemeResolver());
|
|
|
80
91
|
*/
|
|
81
92
|
const startDetectorPrefetch = (
|
|
82
93
|
siteKey: string | null,
|
|
83
|
-
flags: { ipv4?: boolean; ipv6?: boolean },
|
|
94
|
+
flags: { ipv4?: boolean; ipv6?: boolean; startMode?: StartModeEnum },
|
|
84
95
|
): void => {
|
|
85
96
|
if (!siteKey) return;
|
|
97
|
+
if (flags.startMode === StartModeEnum.manual) return;
|
|
86
98
|
void Promise.all([
|
|
87
99
|
import("@prosopo/procaptcha-frictionless"),
|
|
88
100
|
import("@prosopo/procaptcha-common"),
|
|
@@ -117,6 +129,7 @@ const implicitRender = async () => {
|
|
|
117
129
|
const web3 = firstElement.getAttribute("data-web3");
|
|
118
130
|
const ipv4 = firstElement.getAttribute("data-ipv4") === "true";
|
|
119
131
|
const ipv6 = firstElement.getAttribute("data-ipv6") === "true";
|
|
132
|
+
const startMode = resolveStartMode(undefined, firstElement);
|
|
120
133
|
if (!siteKey) {
|
|
121
134
|
console.error("No site key found");
|
|
122
135
|
return;
|
|
@@ -127,27 +140,35 @@ const implicitRender = async () => {
|
|
|
127
140
|
// constant. Start it now so the round-trip overlaps the widget's own
|
|
128
141
|
// dynamic-import chain instead of queueing behind it —
|
|
129
142
|
// `customDetectBot` claims the in-flight promise when it eventually runs.
|
|
130
|
-
startDetectorPrefetch(siteKey, { ipv4, ipv6 });
|
|
143
|
+
startDetectorPrefetch(siteKey, { ipv4, ipv6, startMode });
|
|
131
144
|
|
|
132
145
|
const implicitRenderOptions: ProcaptchaRenderOptions = {
|
|
133
146
|
siteKey: siteKey,
|
|
134
147
|
ipv4,
|
|
135
148
|
ipv6,
|
|
149
|
+
startMode,
|
|
136
150
|
};
|
|
137
151
|
|
|
138
|
-
const
|
|
152
|
+
const widgets = await widgetFactory.createWidgets(
|
|
139
153
|
elements,
|
|
140
154
|
implicitRenderOptions,
|
|
141
155
|
!(web3 === "true"),
|
|
142
156
|
);
|
|
143
157
|
|
|
144
|
-
registerWidgets(
|
|
145
|
-
|
|
158
|
+
const ids = registerWidgets(
|
|
159
|
+
widgets,
|
|
146
160
|
elements,
|
|
147
161
|
implicitRenderOptions,
|
|
148
162
|
!(web3 === "true"),
|
|
149
163
|
false,
|
|
150
164
|
);
|
|
165
|
+
|
|
166
|
+
// `data-placement` is read per element by the renderer; `data-bind` is
|
|
167
|
+
// wired here because the trigger lives outside the widget.
|
|
168
|
+
ids.forEach((id, index) => {
|
|
169
|
+
const selector = at(elements, index).getAttribute("data-bind");
|
|
170
|
+
if (selector) bindTrigger(id, selector);
|
|
171
|
+
});
|
|
151
172
|
}
|
|
152
173
|
|
|
153
174
|
// Check for invisible mode indicators (procaptcha class on buttons)
|
|
@@ -161,29 +182,37 @@ const implicitRender = async () => {
|
|
|
161
182
|
const callback = button.getAttribute("data-callback") || "";
|
|
162
183
|
const ipv4 = button.getAttribute("data-ipv4") === "true";
|
|
163
184
|
const ipv6 = button.getAttribute("data-ipv6") === "true";
|
|
185
|
+
const startMode = resolveStartMode(undefined, button);
|
|
164
186
|
|
|
165
|
-
startDetectorPrefetch(siteKey, { ipv4, ipv6 });
|
|
187
|
+
startDetectorPrefetch(siteKey, { ipv4, ipv6, startMode });
|
|
166
188
|
|
|
167
189
|
const buttonRenderOptions: ProcaptchaRenderOptions = {
|
|
168
190
|
siteKey: siteKey,
|
|
169
191
|
callback: callback,
|
|
170
192
|
ipv4,
|
|
171
193
|
ipv6,
|
|
194
|
+
startMode,
|
|
172
195
|
};
|
|
173
196
|
|
|
174
|
-
const
|
|
197
|
+
const widgets = await widgetFactory.createWidgets(
|
|
175
198
|
[button],
|
|
176
199
|
buttonRenderOptions,
|
|
177
200
|
true,
|
|
178
201
|
true,
|
|
179
202
|
);
|
|
180
203
|
|
|
181
|
-
|
|
204
|
+
const [id] = registerWidgets(
|
|
205
|
+
widgets,
|
|
206
|
+
[button],
|
|
207
|
+
buttonRenderOptions,
|
|
208
|
+
true,
|
|
209
|
+
true,
|
|
210
|
+
);
|
|
182
211
|
|
|
183
212
|
// Add click event listener to the button
|
|
184
213
|
button.addEventListener("click", async (event) => {
|
|
185
214
|
event.preventDefault();
|
|
186
|
-
execute();
|
|
215
|
+
execute(id);
|
|
187
216
|
});
|
|
188
217
|
}
|
|
189
218
|
}
|
|
@@ -203,7 +232,10 @@ export const render = async (
|
|
|
203
232
|
element: Element,
|
|
204
233
|
renderOptions: ProcaptchaRenderOptions,
|
|
205
234
|
): Promise<string | undefined> => {
|
|
206
|
-
startDetectorPrefetch(renderOptions.siteKey,
|
|
235
|
+
startDetectorPrefetch(renderOptions.siteKey, {
|
|
236
|
+
...renderOptions,
|
|
237
|
+
startMode: resolveStartMode(renderOptions, element),
|
|
238
|
+
});
|
|
207
239
|
|
|
208
240
|
const hasInvisibleSize =
|
|
209
241
|
Object.prototype.hasOwnProperty.call(renderOptions, "size") &&
|
|
@@ -213,7 +245,7 @@ export const render = async (
|
|
|
213
245
|
const invisible =
|
|
214
246
|
hasInvisibleSize || element.tagName.toLowerCase() === "button";
|
|
215
247
|
|
|
216
|
-
const
|
|
248
|
+
const widgets = await widgetFactory.createWidgets(
|
|
217
249
|
[element],
|
|
218
250
|
renderOptions,
|
|
219
251
|
isWeb2,
|
|
@@ -221,7 +253,7 @@ export const render = async (
|
|
|
221
253
|
);
|
|
222
254
|
|
|
223
255
|
const ids = registerWidgets(
|
|
224
|
-
|
|
256
|
+
widgets,
|
|
225
257
|
[element],
|
|
226
258
|
renderOptions,
|
|
227
259
|
isWeb2,
|
|
@@ -230,7 +262,10 @@ export const render = async (
|
|
|
230
262
|
|
|
231
263
|
// Deliberately not `at()`: it throws on an empty array before it consults
|
|
232
264
|
// `optional`, and zero roots is a legitimate outcome here.
|
|
233
|
-
|
|
265
|
+
const id = ids[0];
|
|
266
|
+
if (id && renderOptions.bind) bindTrigger(id, renderOptions.bind);
|
|
267
|
+
|
|
268
|
+
return id;
|
|
234
269
|
};
|
|
235
270
|
|
|
236
271
|
export default function ready(fn: () => void) {
|
|
@@ -247,8 +282,21 @@ export default function ready(fn: () => void) {
|
|
|
247
282
|
}
|
|
248
283
|
}
|
|
249
284
|
|
|
250
|
-
|
|
251
|
-
|
|
285
|
+
/**
|
|
286
|
+
* Starts verification. With no id the event goes to `document` and every
|
|
287
|
+
* widget responds; with an id (as returned by `render()`) only that widget
|
|
288
|
+
* runs.
|
|
289
|
+
*/
|
|
290
|
+
export const execute = (widgetId?: string) => {
|
|
291
|
+
const targeted =
|
|
292
|
+
undefined === widgetId ? undefined : procaptchaWidgets.get(widgetId);
|
|
293
|
+
|
|
294
|
+
if (undefined !== widgetId && !targeted) {
|
|
295
|
+
console.error(`No Procaptcha widget found with id ${widgetId}`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const containers = targeted ? [targeted.element] : findProcaptchaContainers();
|
|
252
300
|
|
|
253
301
|
if (containers.length === 0) {
|
|
254
302
|
console.error("No Procaptcha containers found for execution");
|
|
@@ -262,14 +310,77 @@ export const execute = () => {
|
|
|
262
310
|
containerCount: containers.length,
|
|
263
311
|
timestamp: Date.now(),
|
|
264
312
|
},
|
|
265
|
-
|
|
313
|
+
// A targeted event must not bubble to document, where every widget listens.
|
|
314
|
+
bubbles: !targeted,
|
|
266
315
|
cancelable: true,
|
|
267
316
|
});
|
|
268
317
|
|
|
318
|
+
if (targeted) {
|
|
319
|
+
targeted.target.dispatchEvent(executeEvent);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
269
323
|
// Dispatch the event on the document
|
|
270
324
|
document.dispatchEvent(executeEvent);
|
|
271
325
|
};
|
|
272
326
|
|
|
327
|
+
/**
|
|
328
|
+
* The click's default is prevented so a submit button does not post the form
|
|
329
|
+
* before a token exists.
|
|
330
|
+
*/
|
|
331
|
+
const bindTrigger = (widgetId: string, selector: string): void => {
|
|
332
|
+
const entry = procaptchaWidgets.get(widgetId);
|
|
333
|
+
if (!entry) {
|
|
334
|
+
console.error(`No Procaptcha widget found with id ${widgetId}`);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const trigger = document.querySelector(selector);
|
|
339
|
+
if (!trigger) {
|
|
340
|
+
console.error(`Procaptcha: no element matches bind selector ${selector}`);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Binding the same widget twice would otherwise stack listeners and leave
|
|
345
|
+
// the earlier one behind, since only the last unbind is remembered.
|
|
346
|
+
entry.unbindTrigger?.();
|
|
347
|
+
|
|
348
|
+
const onClick = (event: Event) => {
|
|
349
|
+
event.preventDefault();
|
|
350
|
+
execute(widgetId);
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
trigger.addEventListener("click", onClick);
|
|
354
|
+
entry.unbindTrigger = () => trigger.removeEventListener("click", onClick);
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
/** Starts a `startMode: "manual"` widget; omit the id to start all of them. */
|
|
358
|
+
export const start = (widgetId?: string): void => {
|
|
359
|
+
const ids: string[] =
|
|
360
|
+
undefined === widgetId ? Array.from(procaptchaWidgets.keys()) : [widgetId];
|
|
361
|
+
|
|
362
|
+
if (ids.length === 0) {
|
|
363
|
+
console.error("No Procaptcha widgets found to start");
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
for (const id of ids) {
|
|
368
|
+
const entry = procaptchaWidgets.get(id);
|
|
369
|
+
if (!entry) {
|
|
370
|
+
console.error(`No Procaptcha widget found with id ${id}`);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
const detail: ProcaptchaStartEventDetail = { element: entry.element };
|
|
374
|
+
document.dispatchEvent(
|
|
375
|
+
new CustomEvent<ProcaptchaStartEventDetail>(PROCAPTCHA_START_EVENT, {
|
|
376
|
+
detail,
|
|
377
|
+
bubbles: true,
|
|
378
|
+
cancelable: false,
|
|
379
|
+
}),
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
273
384
|
function findProcaptchaContainers(): Element[] {
|
|
274
385
|
const containers: Element[] = [];
|
|
275
386
|
|
|
@@ -310,11 +421,12 @@ declare global {
|
|
|
310
421
|
reset: typeof reset;
|
|
311
422
|
remove: typeof remove;
|
|
312
423
|
execute: typeof execute;
|
|
424
|
+
start: typeof start;
|
|
313
425
|
};
|
|
314
426
|
}
|
|
315
427
|
}
|
|
316
428
|
|
|
317
|
-
const
|
|
429
|
+
const boot = () => {
|
|
318
430
|
// onLoadUrlCallback defines the name of the callback function to be called when the script is loaded
|
|
319
431
|
// onRenderExplicit takes values of either explicit or implicit
|
|
320
432
|
const { onloadUrlCallback, renderExplicit } = extractParams(BUNDLE_NAMES);
|
|
@@ -352,7 +464,7 @@ const start = () => {
|
|
|
352
464
|
* widget id to reset one widget, or omit it to reset every widget on the page.
|
|
353
465
|
*
|
|
354
466
|
* This remounts rather than merely unmounting. The previous implementation
|
|
355
|
-
* unmounted every root and then called `
|
|
467
|
+
* unmounted every root and then called `boot()`, which only re-renders when
|
|
356
468
|
* the page uses implicit rendering — and even then only via the
|
|
357
469
|
* `document.readyState` fallback, because the script's `load` event has long
|
|
358
470
|
* since fired. On an explicitly-rendered page nothing came back at all: the
|
|
@@ -360,7 +472,7 @@ const start = () => {
|
|
|
360
472
|
* request was ever made. Rebuilding from the stored descriptor makes reset
|
|
361
473
|
* behave the same way on both paths.
|
|
362
474
|
*
|
|
363
|
-
* `
|
|
475
|
+
* `boot()` is deliberately not called here any more. It re-renders implicit
|
|
364
476
|
* widgets, which would double up with the remount below, and it attaches
|
|
365
477
|
* another `load` listener to the script tag on every call.
|
|
366
478
|
*/
|
|
@@ -374,15 +486,19 @@ export const reset = async (widgetId?: string): Promise<void> => {
|
|
|
374
486
|
|
|
375
487
|
current.root.unmount();
|
|
376
488
|
|
|
377
|
-
const [
|
|
489
|
+
const [widget] = await widgetFactory.createWidgets(
|
|
378
490
|
[current.element],
|
|
379
491
|
current.renderOptions,
|
|
380
492
|
current.isWeb2,
|
|
381
493
|
current.invisible,
|
|
382
494
|
);
|
|
383
495
|
|
|
384
|
-
if (
|
|
385
|
-
procaptchaWidgets.set(id, {
|
|
496
|
+
if (widget) {
|
|
497
|
+
procaptchaWidgets.set(id, {
|
|
498
|
+
...current,
|
|
499
|
+
root: widget.root,
|
|
500
|
+
target: widget.container,
|
|
501
|
+
});
|
|
386
502
|
} else {
|
|
387
503
|
procaptchaWidgets.delete(id);
|
|
388
504
|
}
|
|
@@ -402,6 +518,7 @@ export const remove = (widgetId?: string): void => {
|
|
|
402
518
|
for (const id of ids) {
|
|
403
519
|
const entry = procaptchaWidgets.get(id);
|
|
404
520
|
if (!entry) continue;
|
|
521
|
+
entry.unbindTrigger?.();
|
|
405
522
|
entry.root.unmount();
|
|
406
523
|
entry.element.innerHTML = "";
|
|
407
524
|
procaptchaWidgets.delete(id);
|
|
@@ -409,7 +526,7 @@ export const remove = (widgetId?: string): void => {
|
|
|
409
526
|
};
|
|
410
527
|
|
|
411
528
|
// set the procaptcha attribute on the window
|
|
412
|
-
window.procaptcha = { ready, render, reset, remove, execute };
|
|
529
|
+
window.procaptcha = { ready, render, reset, remove, execute, start };
|
|
413
530
|
|
|
414
531
|
// Dispatch a custom event to notify that window.procaptcha is ready
|
|
415
532
|
const procaptchaReadyEvent = new CustomEvent(PROCAPTCHA_READY_EVENT, {
|
|
@@ -421,4 +538,4 @@ const procaptchaReadyEvent = new CustomEvent(PROCAPTCHA_READY_EVENT, {
|
|
|
421
538
|
});
|
|
422
539
|
document.dispatchEvent(procaptchaReadyEvent);
|
|
423
540
|
|
|
424
|
-
|
|
541
|
+
boot();
|
|
@@ -0,0 +1,227 @@
|
|
|
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 { Root } from "react-dom/client";
|
|
16
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
17
|
+
import type { CreatedWidget } from "../util/widgetFactory.js";
|
|
18
|
+
|
|
19
|
+
const mocks = vi.hoisted(() => ({
|
|
20
|
+
prefetchDetector: vi.fn(),
|
|
21
|
+
createWidgets: vi.fn(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock("@prosopo/procaptcha-frictionless", () => ({
|
|
25
|
+
prefetchDetector: mocks.prefetchDetector,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock("@prosopo/procaptcha-common", () => ({
|
|
29
|
+
getWindowCallback: vi.fn(),
|
|
30
|
+
pickIpMode: vi.fn(() => undefined),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
vi.mock("../util/widgetFactory.js", () => ({
|
|
34
|
+
WidgetFactory: vi.fn(function () {
|
|
35
|
+
return { createWidgets: mocks.createWidgets };
|
|
36
|
+
}),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
const { render, remove, execute } = await import("../index.js");
|
|
40
|
+
|
|
41
|
+
const SITE_KEY = "5CcNvLUdiXFpzKDMjThGLSK9rhWHA1H4EF3zrgkpkjAdqmuP";
|
|
42
|
+
const EXECUTE_EVENT = "procaptcha:execute";
|
|
43
|
+
|
|
44
|
+
const makeRoot = (): Root =>
|
|
45
|
+
({ unmount: vi.fn(), render: vi.fn() }) as unknown as Root;
|
|
46
|
+
|
|
47
|
+
// Like the real factory, the widget lives in a child of the host element and
|
|
48
|
+
// listens there, so a targeted event must be dispatched on that child.
|
|
49
|
+
const createWidgetsLikeTheFactory = async (
|
|
50
|
+
hosts: Element[],
|
|
51
|
+
): Promise<CreatedWidget[]> =>
|
|
52
|
+
hosts.map((host) => {
|
|
53
|
+
const container = document.createElement("div");
|
|
54
|
+
host.appendChild(container);
|
|
55
|
+
return { root: makeRoot(), container };
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const listenLikeAWidget = (
|
|
59
|
+
host: Element,
|
|
60
|
+
): { calls: () => number; stop: () => void } => {
|
|
61
|
+
const handler = vi.fn();
|
|
62
|
+
const target = host.firstElementChild;
|
|
63
|
+
if (!target) throw new Error("expected the factory to mount a container");
|
|
64
|
+
document.addEventListener(EXECUTE_EVENT, handler);
|
|
65
|
+
target.addEventListener(EXECUTE_EVENT, handler);
|
|
66
|
+
return {
|
|
67
|
+
calls: () => handler.mock.calls.length,
|
|
68
|
+
stop: () => {
|
|
69
|
+
document.removeEventListener(EXECUTE_EVENT, handler);
|
|
70
|
+
target.removeEventListener(EXECUTE_EVENT, handler);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
beforeEach(async () => {
|
|
76
|
+
vi.clearAllMocks();
|
|
77
|
+
await remove();
|
|
78
|
+
document.body.innerHTML = "";
|
|
79
|
+
mocks.createWidgets.mockImplementation(createWidgetsLikeTheFactory);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("execute targeting", () => {
|
|
83
|
+
it("reaches every widget when called with no id", async () => {
|
|
84
|
+
const first = document.createElement("div");
|
|
85
|
+
first.className = "p-procaptcha";
|
|
86
|
+
const second = document.createElement("div");
|
|
87
|
+
second.className = "p-procaptcha";
|
|
88
|
+
document.body.append(first, second);
|
|
89
|
+
await render(first, { siteKey: SITE_KEY });
|
|
90
|
+
await render(second, { siteKey: SITE_KEY });
|
|
91
|
+
|
|
92
|
+
const a = listenLikeAWidget(first);
|
|
93
|
+
const b = listenLikeAWidget(second);
|
|
94
|
+
execute();
|
|
95
|
+
|
|
96
|
+
expect(a.calls()).toBe(1);
|
|
97
|
+
expect(b.calls()).toBe(1);
|
|
98
|
+
a.stop();
|
|
99
|
+
b.stop();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("reaches only the named widget when given an id", async () => {
|
|
103
|
+
const first = document.createElement("div");
|
|
104
|
+
const second = document.createElement("div");
|
|
105
|
+
document.body.append(first, second);
|
|
106
|
+
const firstId = await render(first, { siteKey: SITE_KEY });
|
|
107
|
+
await render(second, { siteKey: SITE_KEY });
|
|
108
|
+
|
|
109
|
+
const a = listenLikeAWidget(first);
|
|
110
|
+
const b = listenLikeAWidget(second);
|
|
111
|
+
execute(firstId);
|
|
112
|
+
|
|
113
|
+
expect(a.calls()).toBe(1);
|
|
114
|
+
expect(b.calls()).toBe(0);
|
|
115
|
+
a.stop();
|
|
116
|
+
b.stop();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("does not bubble a targeted event up to document", async () => {
|
|
120
|
+
const element = document.createElement("div");
|
|
121
|
+
document.body.appendChild(element);
|
|
122
|
+
const widgetId = await render(element, { siteKey: SITE_KEY });
|
|
123
|
+
|
|
124
|
+
const documentOnly = vi.fn();
|
|
125
|
+
document.addEventListener(EXECUTE_EVENT, documentOnly);
|
|
126
|
+
execute(widgetId);
|
|
127
|
+
|
|
128
|
+
expect(documentOnly).not.toHaveBeenCalled();
|
|
129
|
+
document.removeEventListener(EXECUTE_EVENT, documentOnly);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("reports an unknown id rather than firing every widget", async () => {
|
|
133
|
+
const element = document.createElement("div");
|
|
134
|
+
document.body.appendChild(element);
|
|
135
|
+
await render(element, { siteKey: SITE_KEY });
|
|
136
|
+
|
|
137
|
+
const error = vi
|
|
138
|
+
.spyOn(console, "error")
|
|
139
|
+
.mockImplementation(() => undefined);
|
|
140
|
+
const listener = listenLikeAWidget(element);
|
|
141
|
+
execute("procaptcha-widget-does-not-exist");
|
|
142
|
+
|
|
143
|
+
expect(listener.calls()).toBe(0);
|
|
144
|
+
expect(error).toHaveBeenCalled();
|
|
145
|
+
listener.stop();
|
|
146
|
+
error.mockRestore();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("bind", () => {
|
|
151
|
+
it("runs only the bound widget when its button is clicked", async () => {
|
|
152
|
+
const button = document.createElement("button");
|
|
153
|
+
button.id = "pay";
|
|
154
|
+
const bound = document.createElement("div");
|
|
155
|
+
const other = document.createElement("div");
|
|
156
|
+
document.body.append(button, bound, other);
|
|
157
|
+
|
|
158
|
+
await render(bound, { siteKey: SITE_KEY, bind: "#pay" });
|
|
159
|
+
await render(other, { siteKey: SITE_KEY });
|
|
160
|
+
|
|
161
|
+
const a = listenLikeAWidget(bound);
|
|
162
|
+
const b = listenLikeAWidget(other);
|
|
163
|
+
button.click();
|
|
164
|
+
|
|
165
|
+
expect(a.calls()).toBe(1);
|
|
166
|
+
expect(b.calls()).toBe(0);
|
|
167
|
+
a.stop();
|
|
168
|
+
b.stop();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("stops the button's default action", async () => {
|
|
172
|
+
const form = document.createElement("form");
|
|
173
|
+
const button = document.createElement("button");
|
|
174
|
+
button.id = "submit-it";
|
|
175
|
+
button.type = "submit";
|
|
176
|
+
form.appendChild(button);
|
|
177
|
+
const element = document.createElement("div");
|
|
178
|
+
document.body.append(form, element);
|
|
179
|
+
|
|
180
|
+
await render(element, { siteKey: SITE_KEY, bind: "#submit-it" });
|
|
181
|
+
|
|
182
|
+
const event = new MouseEvent("click", {
|
|
183
|
+
bubbles: true,
|
|
184
|
+
cancelable: true,
|
|
185
|
+
});
|
|
186
|
+
button.dispatchEvent(event);
|
|
187
|
+
|
|
188
|
+
expect(event.defaultPrevented).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("detaches the listener when the widget is removed", async () => {
|
|
192
|
+
const button = document.createElement("button");
|
|
193
|
+
button.id = "gone";
|
|
194
|
+
const element = document.createElement("div");
|
|
195
|
+
document.body.append(button, element);
|
|
196
|
+
|
|
197
|
+
await render(element, { siteKey: SITE_KEY, bind: "#gone" });
|
|
198
|
+
await remove();
|
|
199
|
+
|
|
200
|
+
const error = vi
|
|
201
|
+
.spyOn(console, "error")
|
|
202
|
+
.mockImplementation(() => undefined);
|
|
203
|
+
button.click();
|
|
204
|
+
|
|
205
|
+
expect(error).not.toHaveBeenCalled();
|
|
206
|
+
error.mockRestore();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("reports a selector that matches nothing and still renders", async () => {
|
|
210
|
+
const error = vi
|
|
211
|
+
.spyOn(console, "error")
|
|
212
|
+
.mockImplementation(() => undefined);
|
|
213
|
+
const element = document.createElement("div");
|
|
214
|
+
document.body.appendChild(element);
|
|
215
|
+
|
|
216
|
+
const widgetId = await render(element, {
|
|
217
|
+
siteKey: SITE_KEY,
|
|
218
|
+
bind: "#nothing-here",
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
expect(widgetId).toBeTruthy();
|
|
222
|
+
expect(error).toHaveBeenCalledWith(
|
|
223
|
+
expect.stringContaining("#nothing-here"),
|
|
224
|
+
);
|
|
225
|
+
error.mockRestore();
|
|
226
|
+
});
|
|
227
|
+
});
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
* to assigning a detector inline on the widget's critical path.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import type { Root } from "react-dom/client";
|
|
23
22
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
23
|
+
import type { CreatedWidget } from "../util/widgetFactory.js";
|
|
24
24
|
|
|
25
25
|
const mocks = vi.hoisted(() => ({
|
|
26
26
|
prefetchDetector: vi.fn(),
|
|
@@ -59,7 +59,7 @@ const renderAndFlush = async (
|
|
|
59
59
|
|
|
60
60
|
beforeEach(() => {
|
|
61
61
|
vi.clearAllMocks();
|
|
62
|
-
mocks.createWidgets.mockResolvedValue([] as
|
|
62
|
+
mocks.createWidgets.mockResolvedValue([] as CreatedWidget[]);
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
describe("render", () => {
|
|
@@ -50,6 +50,7 @@ describe("procaptcha:ready event", () => {
|
|
|
50
50
|
expect(window.procaptcha?.ready).toBeDefined();
|
|
51
51
|
expect(window.procaptcha?.reset).toBeDefined();
|
|
52
52
|
expect(window.procaptcha?.execute).toBeDefined();
|
|
53
|
+
expect(window.procaptcha?.start).toBeDefined();
|
|
53
54
|
});
|
|
54
55
|
|
|
55
56
|
// Import the module which should set window.procaptcha and dispatch the event
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import type { Root } from "react-dom/client";
|
|
24
24
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
25
|
+
import type { CreatedWidget } from "../util/widgetFactory.js";
|
|
25
26
|
|
|
26
27
|
const mocks = vi.hoisted(() => ({
|
|
27
28
|
prefetchDetector: vi.fn(),
|
|
@@ -50,10 +51,15 @@ const SITE_KEY = "5CcNvLUdiXFpzKDMjThGLSK9rhWHA1H4EF3zrgkpkjAdqmuP";
|
|
|
50
51
|
const makeRoot = (): Root =>
|
|
51
52
|
({ unmount: vi.fn(), render: vi.fn() }) as unknown as Root;
|
|
52
53
|
|
|
54
|
+
const makeWidget = (root: Root = makeRoot()): CreatedWidget => ({
|
|
55
|
+
root,
|
|
56
|
+
container: document.createElement("div"),
|
|
57
|
+
});
|
|
58
|
+
|
|
53
59
|
/** Each createWidgets call yields a distinct root, as the real factory does. */
|
|
54
60
|
const queueRoots = (...roots: Root[]): void => {
|
|
55
61
|
for (const root of roots) {
|
|
56
|
-
mocks.createWidgets.mockResolvedValueOnce([root]);
|
|
62
|
+
mocks.createWidgets.mockResolvedValueOnce([makeWidget(root)]);
|
|
57
63
|
}
|
|
58
64
|
};
|
|
59
65
|
|
|
@@ -62,7 +68,7 @@ beforeEach(async () => {
|
|
|
62
68
|
// Drop any widgets registered by a previous test — module state persists
|
|
63
69
|
// across tests in the same file.
|
|
64
70
|
await remove();
|
|
65
|
-
mocks.createWidgets.mockResolvedValue([
|
|
71
|
+
mocks.createWidgets.mockResolvedValue([makeWidget()]);
|
|
66
72
|
});
|
|
67
73
|
|
|
68
74
|
describe("render", () => {
|
|
@@ -78,7 +84,7 @@ describe("render", () => {
|
|
|
78
84
|
});
|
|
79
85
|
|
|
80
86
|
it("returns undefined when the factory creates no widget", async () => {
|
|
81
|
-
mocks.createWidgets.mockResolvedValueOnce([] as
|
|
87
|
+
mocks.createWidgets.mockResolvedValueOnce([] as CreatedWidget[]);
|
|
82
88
|
|
|
83
89
|
const widgetId = await render(document.createElement("div"), {
|
|
84
90
|
siteKey: SITE_KEY,
|