@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.
Files changed (46) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +11 -9
  2. package/.turbo/turbo-build$colon$tsc.log +9 -9
  3. package/.turbo/turbo-build.log +13 -10
  4. package/CHANGELOG.md +48 -0
  5. package/dist/_virtual/_rolldown/runtime.js +3 -0
  6. package/dist/cjs/index.cjs +5 -7
  7. package/dist/cjs/render/renderFunction.cjs +34 -31
  8. package/dist/cjs/render/renderer.cjs +16 -19
  9. package/dist/index.d.ts +2 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +7 -8
  12. package/dist/render/renderFunction.d.ts +1 -0
  13. package/dist/render/renderFunction.d.ts.map +1 -1
  14. package/dist/render/renderFunction.js +34 -32
  15. package/dist/render/renderFunction.js.map +1 -1
  16. package/dist/render/renderer.d.ts +3 -3
  17. package/dist/render/renderer.d.ts.map +1 -1
  18. package/dist/render/renderer.js +16 -19
  19. package/dist/render/renderer.js.map +1 -1
  20. package/dist/tests/index.unit.test.d.ts +2 -0
  21. package/dist/tests/index.unit.test.d.ts.map +1 -0
  22. package/dist/tests/index.unit.test.js +26 -0
  23. package/dist/tests/index.unit.test.js.map +1 -0
  24. package/dist/tests/renderFunction.unit.test.d.ts +2 -0
  25. package/dist/tests/renderFunction.unit.test.d.ts.map +1 -0
  26. package/dist/tests/renderFunction.unit.test.js +186 -0
  27. package/dist/tests/renderFunction.unit.test.js.map +1 -0
  28. package/dist/tests/renderer.test-d.d.ts +2 -0
  29. package/dist/tests/renderer.test-d.d.ts.map +1 -0
  30. package/dist/tests/renderer.test-d.js +92 -0
  31. package/dist/tests/renderer.test-d.js.map +1 -0
  32. package/dist/tests/renderer.unit.test.d.ts +2 -0
  33. package/dist/tests/renderer.unit.test.d.ts.map +1 -0
  34. package/dist/tests/renderer.unit.test.js +207 -0
  35. package/dist/tests/renderer.unit.test.js.map +1 -0
  36. package/package.json +15 -6
  37. package/src/index.ts +7 -1
  38. package/src/render/renderFunction.ts +13 -3
  39. package/src/render/renderer.ts +21 -7
  40. package/src/tests/index.unit.test.ts +47 -0
  41. package/src/tests/renderFunction.unit.test.ts +275 -0
  42. package/src/tests/renderer.test-d.ts +175 -0
  43. package/src/tests/renderer.unit.test.ts +331 -0
  44. package/tsconfig.tsbuildinfo +1 -1
  45. package/vite.test.config.ts +39 -0
  46. package/.turbo/turbo-typecheck.log +0 -4
@@ -15,25 +15,39 @@
15
15
  import type { ProcaptchaRenderOptions } from "@prosopo/types";
16
16
  import { type RendererFunction, loadRenderFunction } from "./renderFunction.js";
17
17
 
18
- interface RendererSettings {
18
+ export interface RendererSettings {
19
19
  scriptUrl: string;
20
20
  scriptId: string;
21
21
  }
22
22
 
23
+ /** Loads the render function from a remote script. Injected so tests need no network. */
24
+ export type LoadRenderFunction = (
25
+ scriptUrl: string,
26
+ scriptId: string,
27
+ ) => Promise<RendererFunction>;
28
+
23
29
  export const createRenderer = (
24
30
  settings: RendererSettings,
31
+ load: LoadRenderFunction = loadRenderFunction,
25
32
  ): RendererFunction => {
26
- let renderFunction: RendererFunction;
33
+ // The in-flight promise is cached, not just the resolved function: caching
34
+ // only the result lets two concurrent render() calls both see "not loaded"
35
+ // and each inject a script tag for the same id.
36
+ let pending: Promise<RendererFunction> | undefined;
27
37
 
28
38
  const getRenderFunction = async (): Promise<RendererFunction> => {
29
- if (!renderFunction) {
30
- renderFunction = await loadRenderFunction(
31
- settings.scriptUrl,
32
- settings.scriptId,
39
+ if (!pending) {
40
+ // A failed load must not be cached, otherwise every later render()
41
+ // replays the same rejection and the widget can never recover.
42
+ pending = load(settings.scriptUrl, settings.scriptId).catch(
43
+ (error: unknown) => {
44
+ pending = undefined;
45
+ throw error;
46
+ },
33
47
  );
34
48
  }
35
49
 
36
- return renderFunction;
50
+ return pending;
37
51
  };
38
52
 
39
53
  return async (
@@ -0,0 +1,47 @@
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 { afterEach, describe, expect, it } from "vitest";
16
+ import { renderProcaptcha } from "../index.js";
17
+
18
+ afterEach(() => {
19
+ document.head.innerHTML = "";
20
+ document.body.innerHTML = "";
21
+ window.procaptcha = undefined;
22
+ });
23
+
24
+ describe("renderProcaptcha", () => {
25
+ it("is exported as a renderer function", () => {
26
+ expect(typeof renderProcaptcha).toBe("function");
27
+ });
28
+
29
+ it("does not touch the DOM until it is called", () => {
30
+ // The renderer is built at import time, but the script must only be
31
+ // fetched on first render — importing the package must cost nothing.
32
+ expect(document.head.querySelectorAll("script")).toHaveLength(0);
33
+ });
34
+
35
+ it("injects the build-time script url on first render", async () => {
36
+ const rendering = renderProcaptcha(document.createElement("div"), {
37
+ siteKey: "site-key",
38
+ });
39
+ await Promise.resolve();
40
+
41
+ const script = document.head.querySelector("script");
42
+ expect(script).not.toBeNull();
43
+
44
+ script?.dispatchEvent(new Event("error"));
45
+ await expect(rendering).rejects.toThrow("Failed to load script");
46
+ });
47
+ });
@@ -0,0 +1,275 @@
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 {
18
+ type RendererFunction,
19
+ loadRenderFunction,
20
+ loadScript,
21
+ } from "../render/renderFunction.js";
22
+
23
+ const SCRIPT_URL = "https://example.test/procaptcha.bundle.js";
24
+ const SCRIPT_ID = "procaptcha-script";
25
+
26
+ const noopRender: RendererFunction = async (): Promise<void> => {};
27
+
28
+ /** The most recently appended script tag, i.e. the one a load is waiting on. */
29
+ const lastScript = (): HTMLScriptElement => {
30
+ const scripts = document.head.querySelectorAll("script");
31
+ const script = scripts[scripts.length - 1];
32
+ if (undefined === script) {
33
+ throw new Error("no script tag was appended");
34
+ }
35
+ return script;
36
+ };
37
+
38
+ /**
39
+ * jsdom never fetches, so nothing ever fires load/error by itself. Wait for the
40
+ * tag to appear, then drive the outcome by hand.
41
+ */
42
+ const settleScript = async (outcome: "load" | "error"): Promise<void> => {
43
+ await Promise.resolve();
44
+ lastScript().dispatchEvent(new Event(outcome));
45
+ };
46
+
47
+ afterEach(() => {
48
+ document.head.innerHTML = "";
49
+ document.body.innerHTML = "";
50
+ window.procaptcha = undefined;
51
+ vi.restoreAllMocks();
52
+ });
53
+
54
+ describe("loadScript", () => {
55
+ it("appends a script tag carrying the requested src", async () => {
56
+ const loading = loadScript(SCRIPT_URL);
57
+ await settleScript("load");
58
+ await loading;
59
+
60
+ expect(lastScript().src).toBe(SCRIPT_URL);
61
+ });
62
+
63
+ it("applies the supplied attributes to the tag", async () => {
64
+ const loading = loadScript(SCRIPT_URL, {
65
+ id: SCRIPT_ID,
66
+ type: "module",
67
+ async: true,
68
+ defer: true,
69
+ });
70
+ await settleScript("load");
71
+ await loading;
72
+
73
+ const script = lastScript();
74
+ expect(script.id).toBe(SCRIPT_ID);
75
+ expect(script.type).toBe("module");
76
+ expect(script.async).toBe(true);
77
+ expect(script.defer).toBe(true);
78
+ });
79
+
80
+ it("works with no attributes at all", async () => {
81
+ const loading = loadScript(SCRIPT_URL);
82
+ await settleScript("load");
83
+
84
+ await expect(loading).resolves.toBeUndefined();
85
+ });
86
+
87
+ it("lets an attribute override the src", async () => {
88
+ const loading = loadScript(SCRIPT_URL, {
89
+ src: "https://example.test/other.js",
90
+ });
91
+ await settleScript("load");
92
+ await loading;
93
+
94
+ expect(lastScript().src).toBe("https://example.test/other.js");
95
+ });
96
+
97
+ it("appends the tag to document.head, not the body", async () => {
98
+ const loading = loadScript(SCRIPT_URL);
99
+ await settleScript("load");
100
+ await loading;
101
+
102
+ expect(document.head.querySelectorAll("script")).toHaveLength(1);
103
+ expect(document.body.querySelectorAll("script")).toHaveLength(0);
104
+ });
105
+
106
+ it("rejects with an Error, not the raw DOM Event", async () => {
107
+ const loading = loadScript(SCRIPT_URL);
108
+ await settleScript("error");
109
+
110
+ await expect(loading).rejects.toBeInstanceOf(Error);
111
+ });
112
+
113
+ it("names the failing url in the rejection message", async () => {
114
+ const loading = loadScript(SCRIPT_URL);
115
+ await settleScript("error");
116
+
117
+ await expect(loading).rejects.toThrow(
118
+ `Failed to load script: ${SCRIPT_URL}`,
119
+ );
120
+ });
121
+
122
+ it("removes the failed tag so a retry leaves no duplicate id behind", async () => {
123
+ const first = loadScript(SCRIPT_URL, { id: SCRIPT_ID });
124
+ await settleScript("error");
125
+ await expect(first).rejects.toThrow();
126
+
127
+ expect(document.head.querySelectorAll("script")).toHaveLength(0);
128
+
129
+ const second = loadScript(SCRIPT_URL, { id: SCRIPT_ID });
130
+ await settleScript("load");
131
+ await second;
132
+
133
+ expect(document.querySelectorAll(`#${SCRIPT_ID}`)).toHaveLength(1);
134
+ });
135
+
136
+ it("stays pending until the script settles", async () => {
137
+ let settled = false;
138
+ const loading = loadScript(SCRIPT_URL).then(() => {
139
+ settled = true;
140
+ });
141
+
142
+ await Promise.resolve();
143
+ expect(settled).toBe(false);
144
+
145
+ await settleScript("load");
146
+ await loading;
147
+ expect(settled).toBe(true);
148
+ });
149
+
150
+ it("accepts an empty url without throwing synchronously", async () => {
151
+ const loading = loadScript("");
152
+ await settleScript("load");
153
+
154
+ await expect(loading).resolves.toBeUndefined();
155
+ });
156
+ });
157
+
158
+ describe("loadRenderFunction", () => {
159
+ it("returns the render function the script installed on window", async () => {
160
+ const loading = loadRenderFunction(SCRIPT_URL, SCRIPT_ID);
161
+ await Promise.resolve();
162
+ window.procaptcha = { render: noopRender };
163
+ lastScript().dispatchEvent(new Event("load"));
164
+
165
+ await expect(loading).resolves.toBe(noopRender);
166
+ });
167
+
168
+ it("requests the script as a deferred async module with the given id", async () => {
169
+ const loading = loadRenderFunction(SCRIPT_URL, SCRIPT_ID);
170
+ await Promise.resolve();
171
+ const script = lastScript();
172
+
173
+ expect(script.src).toBe(SCRIPT_URL);
174
+ expect(script.id).toBe(SCRIPT_ID);
175
+ expect(script.type).toBe("module");
176
+ expect(script.async).toBe(true);
177
+ expect(script.defer).toBe(true);
178
+
179
+ window.procaptcha = { render: noopRender };
180
+ script.dispatchEvent(new Event("load"));
181
+ await loading;
182
+ });
183
+
184
+ it("removes the tag when the script loads but is the wrong script", async () => {
185
+ // Same reasoning as a failed load: a retry must not find a stale tag
186
+ // sitting on the id it is about to reuse.
187
+ const loading = loadRenderFunction(SCRIPT_URL, SCRIPT_ID);
188
+ await settleScript("load");
189
+ await expect(loading).rejects.toThrow();
190
+
191
+ expect(document.getElementById(SCRIPT_ID)).toBeNull();
192
+ });
193
+
194
+ it("leaves a retry with exactly one tag after a wrong-script load", async () => {
195
+ const first = loadRenderFunction(SCRIPT_URL, SCRIPT_ID);
196
+ await settleScript("load");
197
+ await expect(first).rejects.toThrow();
198
+
199
+ const second = loadRenderFunction(SCRIPT_URL, SCRIPT_ID);
200
+ await Promise.resolve();
201
+ window.procaptcha = { render: noopRender };
202
+ lastScript().dispatchEvent(new Event("load"));
203
+
204
+ await expect(second).resolves.toBe(noopRender);
205
+ expect(document.querySelectorAll(`#${SCRIPT_ID}`)).toHaveLength(1);
206
+ });
207
+
208
+ it("keeps the tag when the script loads successfully", async () => {
209
+ const loading = loadRenderFunction(SCRIPT_URL, SCRIPT_ID);
210
+ await Promise.resolve();
211
+ window.procaptcha = { render: noopRender };
212
+ lastScript().dispatchEvent(new Event("load"));
213
+ await loading;
214
+
215
+ expect(document.getElementById(SCRIPT_ID)).not.toBeNull();
216
+ });
217
+
218
+ it("throws when the script loads but installs no procaptcha global", async () => {
219
+ const loading = loadRenderFunction(SCRIPT_URL, SCRIPT_ID);
220
+ await settleScript("load");
221
+
222
+ await expect(loading).rejects.toThrow(
223
+ "Render script does not contain the render function",
224
+ );
225
+ });
226
+
227
+ it("throws when the global exists but carries no render function", async () => {
228
+ const loading = loadRenderFunction(SCRIPT_URL, SCRIPT_ID);
229
+ await Promise.resolve();
230
+ // A partially-initialised global is indistinguishable from a stale one.
231
+ window.procaptcha = { render: undefined } as unknown as {
232
+ render: RendererFunction;
233
+ };
234
+ lastScript().dispatchEvent(new Event("load"));
235
+
236
+ await expect(loading).rejects.toThrow(
237
+ "Render script does not contain the render function",
238
+ );
239
+ });
240
+
241
+ it("propagates the load failure rather than reporting a missing function", async () => {
242
+ const loading = loadRenderFunction(SCRIPT_URL, SCRIPT_ID);
243
+ await settleScript("error");
244
+
245
+ await expect(loading).rejects.toThrow(
246
+ `Failed to load script: ${SCRIPT_URL}`,
247
+ );
248
+ });
249
+
250
+ it("does not check the global until the script has loaded", async () => {
251
+ const loading = loadRenderFunction(SCRIPT_URL, SCRIPT_ID);
252
+ await Promise.resolve();
253
+ // Set late — after injection but before load — to prove the check is
254
+ // deferred until the script has actually run.
255
+ window.procaptcha = { render: noopRender };
256
+ lastScript().dispatchEvent(new Event("load"));
257
+
258
+ await expect(loading).resolves.toBe(noopRender);
259
+ });
260
+
261
+ it("returns a function that forwards its arguments to the loaded render", async () => {
262
+ const render = vi.fn<RendererFunction>(async (): Promise<void> => {});
263
+ const loading = loadRenderFunction(SCRIPT_URL, SCRIPT_ID);
264
+ await Promise.resolve();
265
+ window.procaptcha = { render };
266
+ lastScript().dispatchEvent(new Event("load"));
267
+
268
+ const loaded = await loading;
269
+ const element = document.createElement("div");
270
+ const options: ProcaptchaRenderOptions = { siteKey: "site-key" };
271
+ await loaded(element, options);
272
+
273
+ expect(render).toHaveBeenCalledWith(element, options);
274
+ });
275
+ });
@@ -0,0 +1,175 @@
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 { Languages } from "@prosopo/locale";
16
+ import type { CaptchaType, ProcaptchaRenderOptions } from "@prosopo/types";
17
+ import { assertType, describe, expectTypeOf, it } from "vitest";
18
+ import type {
19
+ ProcaptchaRenderOptions as ExportedRenderOptions,
20
+ ProcaptchaLanguages,
21
+ ProcaptchaType,
22
+ } from "../index.js";
23
+ import { renderProcaptcha } from "../index.js";
24
+ import {
25
+ type RendererFunction,
26
+ loadRenderFunction,
27
+ loadScript,
28
+ } from "../render/renderFunction.js";
29
+ import {
30
+ type LoadRenderFunction,
31
+ type RendererSettings,
32
+ createRenderer,
33
+ } from "../render/renderer.js";
34
+
35
+ // This package is published, so these types are its contract with consumers.
36
+ // Runtime tests cannot catch a signature that silently widens or drifts.
37
+
38
+ describe("RendererFunction", () => {
39
+ it("takes an element and options, and resolves to void", () => {
40
+ expectTypeOf<RendererFunction>().parameters.toEqualTypeOf<
41
+ [HTMLElement, ProcaptchaRenderOptions]
42
+ >();
43
+ expectTypeOf<RendererFunction>().returns.toEqualTypeOf<Promise<void>>();
44
+ });
45
+
46
+ it("does not accept a bare Element for the mount point", () => {
47
+ // Narrower than Element on purpose: the render script sets style and
48
+ // dataset properties that only exist on HTMLElement.
49
+ expectTypeOf<Element>().not.toMatchTypeOf<
50
+ Parameters<RendererFunction>[0]
51
+ >();
52
+ });
53
+
54
+ it("requires a siteKey in the options", () => {
55
+ assertType<Parameters<RendererFunction>[1]>({ siteKey: "site-key" });
56
+ // @ts-expect-error siteKey is mandatory
57
+ assertType<Parameters<RendererFunction>[1]>({});
58
+ });
59
+
60
+ it("rejects an unknown option key", () => {
61
+ assertType<Parameters<RendererFunction>[1]>({
62
+ siteKey: "site-key",
63
+ // @ts-expect-error not part of the render options
64
+ notAnOption: true,
65
+ });
66
+ });
67
+ });
68
+
69
+ describe("renderProcaptcha", () => {
70
+ it("is exposed as a RendererFunction", () => {
71
+ expectTypeOf(renderProcaptcha).toEqualTypeOf<RendererFunction>();
72
+ });
73
+ });
74
+
75
+ describe("re-exported types", () => {
76
+ it("aliases the shared types rather than redeclaring them", () => {
77
+ // These are the names consumers import; they must stay tied to the
78
+ // shared definitions so a change upstream is not silently absorbed.
79
+ expectTypeOf<ProcaptchaType>().toEqualTypeOf<CaptchaType>();
80
+ expectTypeOf<ExportedRenderOptions>().toEqualTypeOf<ProcaptchaRenderOptions>();
81
+ });
82
+
83
+ it("exports the language union as a usable type", () => {
84
+ // `Languages` is a const object with no type meaning, so re-exporting the
85
+ // name as a type produced a binding consumers could not use at all.
86
+ expectTypeOf<ProcaptchaLanguages>().toEqualTypeOf<
87
+ (typeof Languages)[keyof typeof Languages]
88
+ >();
89
+ assertType<ProcaptchaLanguages>("en");
90
+ });
91
+
92
+ it("keeps the language union aligned with the render options", () => {
93
+ expectTypeOf<ProcaptchaLanguages>().toEqualTypeOf<
94
+ NonNullable<ProcaptchaRenderOptions["language"]>
95
+ >();
96
+ });
97
+
98
+ it("rejects a language outside the union", () => {
99
+ // @ts-expect-error not a supported language
100
+ assertType<ProcaptchaLanguages>("not-a-language");
101
+ });
102
+ });
103
+
104
+ describe("createRenderer", () => {
105
+ it("returns a RendererFunction", () => {
106
+ expectTypeOf(createRenderer).returns.toEqualTypeOf<RendererFunction>();
107
+ });
108
+
109
+ it("takes settings and an optional loader override", () => {
110
+ expectTypeOf(createRenderer).parameters.toEqualTypeOf<
111
+ [RendererSettings, (LoadRenderFunction | undefined)?]
112
+ >();
113
+ });
114
+
115
+ it("accepts a call with settings alone", () => {
116
+ expectTypeOf(createRenderer).toBeCallableWith({
117
+ scriptUrl: "url",
118
+ scriptId: "id",
119
+ });
120
+ });
121
+
122
+ it("requires both settings fields", () => {
123
+ // @ts-expect-error scriptId is mandatory
124
+ assertType<RendererSettings>({ scriptUrl: "url" });
125
+ // @ts-expect-error scriptUrl is mandatory
126
+ assertType<RendererSettings>({ scriptId: "id" });
127
+ });
128
+
129
+ it("rejects a loader that resolves to the wrong shape", () => {
130
+ // @ts-expect-error the loader must resolve to a RendererFunction
131
+ createRenderer({ scriptUrl: "url", scriptId: "id" }, async () => "nope");
132
+ });
133
+ });
134
+
135
+ describe("LoadRenderFunction", () => {
136
+ it("takes two strings and resolves to a RendererFunction", () => {
137
+ expectTypeOf<LoadRenderFunction>().parameters.toEqualTypeOf<
138
+ [string, string]
139
+ >();
140
+ expectTypeOf<LoadRenderFunction>().returns.toEqualTypeOf<
141
+ Promise<RendererFunction>
142
+ >();
143
+ });
144
+
145
+ it("is satisfied by the real loader", () => {
146
+ // The default argument of createRenderer depends on this holding.
147
+ expectTypeOf(loadRenderFunction).toMatchTypeOf<LoadRenderFunction>();
148
+ });
149
+ });
150
+
151
+ describe("loadScript", () => {
152
+ it("resolves to void rather than the tag it created", () => {
153
+ expectTypeOf(loadScript).returns.toEqualTypeOf<Promise<void>>();
154
+ });
155
+
156
+ it("takes an optional partial set of script attributes", () => {
157
+ expectTypeOf(loadScript).toBeCallableWith("url");
158
+ expectTypeOf(loadScript).toBeCallableWith("url", { id: "id" });
159
+ });
160
+
161
+ it("rejects an attribute that is not on a script element", () => {
162
+ // @ts-expect-error href belongs to link elements, not script
163
+ loadScript("url", { href: "url" });
164
+ });
165
+ });
166
+
167
+ describe("window.procaptcha", () => {
168
+ it("is declared as possibly absent", () => {
169
+ // The global only exists once the render script has run, so consumers
170
+ // must be forced to narrow before reaching for render.
171
+ expectTypeOf<typeof window.procaptcha>().toEqualTypeOf<
172
+ { render: RendererFunction } | undefined
173
+ >();
174
+ });
175
+ });