@prosopo/procaptcha-wrapper 2.6.102 → 2.6.104
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 +11 -9
- package/.turbo/turbo-build$colon$tsc.log +9 -9
- package/.turbo/turbo-build.log +13 -10
- package/CHANGELOG.md +48 -0
- package/dist/_virtual/_rolldown/runtime.js +3 -0
- package/dist/cjs/index.cjs +5 -7
- package/dist/cjs/render/renderFunction.cjs +34 -31
- package/dist/cjs/render/renderer.cjs +16 -19
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -8
- package/dist/render/renderFunction.d.ts +1 -0
- package/dist/render/renderFunction.d.ts.map +1 -1
- package/dist/render/renderFunction.js +34 -32
- package/dist/render/renderFunction.js.map +1 -1
- package/dist/render/renderer.d.ts +3 -3
- package/dist/render/renderer.d.ts.map +1 -1
- package/dist/render/renderer.js +16 -19
- package/dist/render/renderer.js.map +1 -1
- package/dist/tests/index.unit.test.d.ts +2 -0
- package/dist/tests/index.unit.test.d.ts.map +1 -0
- package/dist/tests/index.unit.test.js +26 -0
- package/dist/tests/index.unit.test.js.map +1 -0
- package/dist/tests/renderFunction.unit.test.d.ts +2 -0
- package/dist/tests/renderFunction.unit.test.d.ts.map +1 -0
- package/dist/tests/renderFunction.unit.test.js +186 -0
- package/dist/tests/renderFunction.unit.test.js.map +1 -0
- package/dist/tests/renderer.test-d.d.ts +2 -0
- package/dist/tests/renderer.test-d.d.ts.map +1 -0
- package/dist/tests/renderer.test-d.js +92 -0
- package/dist/tests/renderer.test-d.js.map +1 -0
- package/dist/tests/renderer.unit.test.d.ts +2 -0
- package/dist/tests/renderer.unit.test.d.ts.map +1 -0
- package/dist/tests/renderer.unit.test.js +207 -0
- package/dist/tests/renderer.unit.test.js.map +1 -0
- package/package.json +15 -6
- package/src/index.ts +7 -1
- package/src/render/renderFunction.ts +13 -3
- package/src/render/renderer.ts +21 -7
- package/src/tests/index.unit.test.ts +47 -0
- package/src/tests/renderFunction.unit.test.ts +275 -0
- package/src/tests/renderer.test-d.ts +175 -0
- package/src/tests/renderer.unit.test.ts +331 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vite.test.config.ts +39 -0
- package/.turbo/turbo-typecheck.log +0 -4
|
@@ -0,0 +1,331 @@
|
|
|
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 { ProcaptchaRenderOptions } from "@prosopo/types";
|
|
16
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
17
|
+
import type { RendererFunction } from "../render/renderFunction.js";
|
|
18
|
+
import {
|
|
19
|
+
type LoadRenderFunction,
|
|
20
|
+
type RendererSettings,
|
|
21
|
+
createRenderer,
|
|
22
|
+
} from "../render/renderer.js";
|
|
23
|
+
|
|
24
|
+
const SETTINGS: RendererSettings = {
|
|
25
|
+
scriptUrl: "https://example.test/procaptcha.bundle.js",
|
|
26
|
+
scriptId: "procaptcha-script",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const options = (siteKey = "site-key"): ProcaptchaRenderOptions => ({
|
|
30
|
+
siteKey,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/** A load that resolves to a spy render, counting how often it was invoked. */
|
|
34
|
+
const stubLoad = (
|
|
35
|
+
render: RendererFunction,
|
|
36
|
+
): { load: LoadRenderFunction; calls: () => number } => {
|
|
37
|
+
let calls = 0;
|
|
38
|
+
const load: LoadRenderFunction = async (): Promise<RendererFunction> => {
|
|
39
|
+
calls += 1;
|
|
40
|
+
return render;
|
|
41
|
+
};
|
|
42
|
+
return { load, calls: () => calls };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** A load whose resolution the test controls, for concurrency assertions. */
|
|
46
|
+
const deferredLoad = (): {
|
|
47
|
+
load: LoadRenderFunction;
|
|
48
|
+
resolve: (render: RendererFunction) => void;
|
|
49
|
+
reject: (error: Error) => void;
|
|
50
|
+
calls: () => number;
|
|
51
|
+
} => {
|
|
52
|
+
let calls = 0;
|
|
53
|
+
let resolveFn: (render: RendererFunction) => void = () => {};
|
|
54
|
+
let rejectFn: (error: Error) => void = () => {};
|
|
55
|
+
const load: LoadRenderFunction = (): Promise<RendererFunction> => {
|
|
56
|
+
calls += 1;
|
|
57
|
+
return new Promise<RendererFunction>((resolve, reject) => {
|
|
58
|
+
resolveFn = resolve;
|
|
59
|
+
rejectFn = reject;
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
load,
|
|
64
|
+
resolve: (render: RendererFunction): void => {
|
|
65
|
+
resolveFn(render);
|
|
66
|
+
},
|
|
67
|
+
reject: (error: Error): void => {
|
|
68
|
+
rejectFn(error);
|
|
69
|
+
},
|
|
70
|
+
calls: () => calls,
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
document.head.innerHTML = "";
|
|
76
|
+
document.body.innerHTML = "";
|
|
77
|
+
window.procaptcha = undefined;
|
|
78
|
+
vi.restoreAllMocks();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("createRenderer", () => {
|
|
82
|
+
it("renders into the given element with the given options", async () => {
|
|
83
|
+
const render = vi.fn<RendererFunction>(async (): Promise<void> => {});
|
|
84
|
+
const { load } = stubLoad(render);
|
|
85
|
+
const element = document.createElement("div");
|
|
86
|
+
|
|
87
|
+
await createRenderer(SETTINGS, load)(element, options());
|
|
88
|
+
|
|
89
|
+
expect(render).toHaveBeenCalledTimes(1);
|
|
90
|
+
expect(render.mock.calls[0]?.[0]).toBe(element);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("passes the configured script url and id to the loader", async () => {
|
|
94
|
+
const load = vi.fn<LoadRenderFunction>(
|
|
95
|
+
async (): Promise<RendererFunction> => async (): Promise<void> => {},
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
await createRenderer(SETTINGS, load)(
|
|
99
|
+
document.createElement("div"),
|
|
100
|
+
options(),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(load).toHaveBeenCalledWith(SETTINGS.scriptUrl, SETTINGS.scriptId);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("loads the script only once across repeated renders", async () => {
|
|
107
|
+
const { load, calls } = stubLoad(async (): Promise<void> => {});
|
|
108
|
+
const renderer = createRenderer(SETTINGS, load);
|
|
109
|
+
|
|
110
|
+
await renderer(document.createElement("div"), options());
|
|
111
|
+
await renderer(document.createElement("div"), options());
|
|
112
|
+
await renderer(document.createElement("div"), options());
|
|
113
|
+
|
|
114
|
+
expect(calls()).toBe(1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("loads only once when renders overlap", async () => {
|
|
118
|
+
// Caching the resolved function rather than the in-flight promise would
|
|
119
|
+
// let both calls miss the cache and inject a second script tag.
|
|
120
|
+
const deferred = deferredLoad();
|
|
121
|
+
const renderer = createRenderer(SETTINGS, deferred.load);
|
|
122
|
+
|
|
123
|
+
const first = renderer(document.createElement("div"), options());
|
|
124
|
+
const second = renderer(document.createElement("div"), options());
|
|
125
|
+
await Promise.resolve();
|
|
126
|
+
|
|
127
|
+
expect(deferred.calls()).toBe(1);
|
|
128
|
+
|
|
129
|
+
deferred.resolve(async (): Promise<void> => {});
|
|
130
|
+
await Promise.all([first, second]);
|
|
131
|
+
expect(deferred.calls()).toBe(1);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("renders every overlapping call once the script arrives", async () => {
|
|
135
|
+
const render = vi.fn<RendererFunction>(async (): Promise<void> => {});
|
|
136
|
+
const deferred = deferredLoad();
|
|
137
|
+
const renderer = createRenderer(SETTINGS, deferred.load);
|
|
138
|
+
|
|
139
|
+
const pending = [
|
|
140
|
+
renderer(document.createElement("div"), options()),
|
|
141
|
+
renderer(document.createElement("div"), options()),
|
|
142
|
+
];
|
|
143
|
+
deferred.resolve(render);
|
|
144
|
+
await Promise.all(pending);
|
|
145
|
+
|
|
146
|
+
expect(render).toHaveBeenCalledTimes(2);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("clones the options so the render function cannot mutate the caller's object", async () => {
|
|
150
|
+
// React and friends freeze props; the wrapper must hand over a writable copy.
|
|
151
|
+
const supplied = options();
|
|
152
|
+
const render: RendererFunction = async (
|
|
153
|
+
_element: HTMLElement,
|
|
154
|
+
received: ProcaptchaRenderOptions,
|
|
155
|
+
): Promise<void> => {
|
|
156
|
+
Object.assign(received, { siteKey: "mutated" });
|
|
157
|
+
};
|
|
158
|
+
const { load } = stubLoad(render);
|
|
159
|
+
|
|
160
|
+
await createRenderer(SETTINGS, load)(
|
|
161
|
+
document.createElement("div"),
|
|
162
|
+
supplied,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
expect(supplied.siteKey).toBe("site-key");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("passes an extensible copy even when the caller's options are frozen", async () => {
|
|
169
|
+
const supplied = Object.freeze(options());
|
|
170
|
+
const render: RendererFunction = async (
|
|
171
|
+
_element: HTMLElement,
|
|
172
|
+
received: ProcaptchaRenderOptions,
|
|
173
|
+
): Promise<void> => {
|
|
174
|
+
Object.assign(received, { siteKey: "mutated" });
|
|
175
|
+
};
|
|
176
|
+
const { load } = stubLoad(render);
|
|
177
|
+
|
|
178
|
+
await expect(
|
|
179
|
+
createRenderer(SETTINGS, load)(document.createElement("div"), supplied),
|
|
180
|
+
).resolves.toBeUndefined();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("forwards the option values on the copy it makes", async () => {
|
|
184
|
+
const render = vi.fn<RendererFunction>(async (): Promise<void> => {});
|
|
185
|
+
const { load } = stubLoad(render);
|
|
186
|
+
|
|
187
|
+
await createRenderer(SETTINGS, load)(
|
|
188
|
+
document.createElement("div"),
|
|
189
|
+
options("abc"),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
expect(render.mock.calls[0]?.[1]).toEqual({ siteKey: "abc" });
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("gives each render its own copy of the options", async () => {
|
|
196
|
+
const seen: ProcaptchaRenderOptions[] = [];
|
|
197
|
+
const render: RendererFunction = async (
|
|
198
|
+
_element: HTMLElement,
|
|
199
|
+
received: ProcaptchaRenderOptions,
|
|
200
|
+
): Promise<void> => {
|
|
201
|
+
seen.push(received);
|
|
202
|
+
};
|
|
203
|
+
const { load } = stubLoad(render);
|
|
204
|
+
const renderer = createRenderer(SETTINGS, load);
|
|
205
|
+
const supplied = options();
|
|
206
|
+
|
|
207
|
+
await renderer(document.createElement("div"), supplied);
|
|
208
|
+
await renderer(document.createElement("div"), supplied);
|
|
209
|
+
|
|
210
|
+
expect(seen[0]).not.toBe(seen[1]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("propagates a load failure to the caller", async () => {
|
|
214
|
+
const load: LoadRenderFunction = async (): Promise<RendererFunction> => {
|
|
215
|
+
throw new Error("network down");
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
await expect(
|
|
219
|
+
createRenderer(SETTINGS, load)(document.createElement("div"), options()),
|
|
220
|
+
).rejects.toThrow("network down");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("retries the load after a failure rather than caching the rejection", async () => {
|
|
224
|
+
let calls = 0;
|
|
225
|
+
const render = vi.fn<RendererFunction>(async (): Promise<void> => {});
|
|
226
|
+
const load: LoadRenderFunction = async (): Promise<RendererFunction> => {
|
|
227
|
+
calls += 1;
|
|
228
|
+
if (1 === calls) {
|
|
229
|
+
throw new Error("network down");
|
|
230
|
+
}
|
|
231
|
+
return render;
|
|
232
|
+
};
|
|
233
|
+
const renderer = createRenderer(SETTINGS, load);
|
|
234
|
+
|
|
235
|
+
await expect(
|
|
236
|
+
renderer(document.createElement("div"), options()),
|
|
237
|
+
).rejects.toThrow("network down");
|
|
238
|
+
await renderer(document.createElement("div"), options());
|
|
239
|
+
|
|
240
|
+
expect(calls).toBe(2);
|
|
241
|
+
expect(render).toHaveBeenCalledTimes(1);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("fails every overlapping render when the shared load fails", async () => {
|
|
245
|
+
const deferred = deferredLoad();
|
|
246
|
+
const renderer = createRenderer(SETTINGS, deferred.load);
|
|
247
|
+
|
|
248
|
+
const first = renderer(document.createElement("div"), options());
|
|
249
|
+
const second = renderer(document.createElement("div"), options());
|
|
250
|
+
deferred.reject(new Error("network down"));
|
|
251
|
+
|
|
252
|
+
await expect(first).rejects.toThrow("network down");
|
|
253
|
+
await expect(second).rejects.toThrow("network down");
|
|
254
|
+
expect(deferred.calls()).toBe(1);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("propagates an error thrown by the render function itself", async () => {
|
|
258
|
+
const { load } = stubLoad(async (): Promise<void> => {
|
|
259
|
+
throw new Error("render blew up");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
await expect(
|
|
263
|
+
createRenderer(SETTINGS, load)(document.createElement("div"), options()),
|
|
264
|
+
).rejects.toThrow("render blew up");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("keeps the loaded script cached after a render failure", async () => {
|
|
268
|
+
// The script loaded fine; only the render call failed. Reloading it would
|
|
269
|
+
// inject a duplicate tag for a fault that has nothing to do with loading.
|
|
270
|
+
let calls = 0;
|
|
271
|
+
let shouldThrow = true;
|
|
272
|
+
const load: LoadRenderFunction = async (): Promise<RendererFunction> => {
|
|
273
|
+
calls += 1;
|
|
274
|
+
return async (): Promise<void> => {
|
|
275
|
+
if (shouldThrow) {
|
|
276
|
+
throw new Error("render blew up");
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
const renderer = createRenderer(SETTINGS, load);
|
|
281
|
+
|
|
282
|
+
await expect(
|
|
283
|
+
renderer(document.createElement("div"), options()),
|
|
284
|
+
).rejects.toThrow("render blew up");
|
|
285
|
+
shouldThrow = false;
|
|
286
|
+
await renderer(document.createElement("div"), options());
|
|
287
|
+
|
|
288
|
+
expect(calls).toBe(1);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("keeps separate renderers independent", async () => {
|
|
292
|
+
const a = stubLoad(async (): Promise<void> => {});
|
|
293
|
+
const b = stubLoad(async (): Promise<void> => {});
|
|
294
|
+
|
|
295
|
+
await createRenderer(SETTINGS, a.load)(
|
|
296
|
+
document.createElement("div"),
|
|
297
|
+
options(),
|
|
298
|
+
);
|
|
299
|
+
await createRenderer(SETTINGS, b.load)(
|
|
300
|
+
document.createElement("div"),
|
|
301
|
+
options(),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
expect(a.calls()).toBe(1);
|
|
305
|
+
expect(b.calls()).toBe(1);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("renders into a detached element without touching the document", async () => {
|
|
309
|
+
const render = vi.fn<RendererFunction>(async (): Promise<void> => {});
|
|
310
|
+
const { load } = stubLoad(render);
|
|
311
|
+
const detached = document.createElement("div");
|
|
312
|
+
|
|
313
|
+
await createRenderer(SETTINGS, load)(detached, options());
|
|
314
|
+
|
|
315
|
+
expect(render).toHaveBeenCalledTimes(1);
|
|
316
|
+
expect(document.body.contains(detached)).toBe(false);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("defaults to the real loader when none is injected", async () => {
|
|
320
|
+
const renderer = createRenderer(SETTINGS);
|
|
321
|
+
const rendering = renderer(document.createElement("div"), options());
|
|
322
|
+
await Promise.resolve();
|
|
323
|
+
|
|
324
|
+
// The default path goes through the DOM loader, so a tag must appear.
|
|
325
|
+
const script = document.head.querySelector("script");
|
|
326
|
+
expect(script?.src).toBe(SETTINGS.scriptUrl);
|
|
327
|
+
|
|
328
|
+
script?.dispatchEvent(new Event("error"));
|
|
329
|
+
await expect(rendering).rejects.toThrow("Failed to load script");
|
|
330
|
+
});
|
|
331
|
+
});
|