@prosopo/procaptcha-common 2.9.19 → 2.10.17

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 (107) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +22 -15
  2. package/.turbo/turbo-build$colon$tsc.log +52 -0
  3. package/.turbo/turbo-build.log +26 -16
  4. package/CHANGELOG.md +392 -0
  5. package/dist/callbacks/defaultCallbacks.d.ts +4 -0
  6. package/dist/callbacks/defaultCallbacks.d.ts.map +1 -0
  7. package/dist/callbacks/defaultCallbacks.js.map +1 -0
  8. package/dist/callbacks/defaultEvents.d.ts +14 -0
  9. package/dist/callbacks/defaultEvents.d.ts.map +1 -0
  10. package/dist/callbacks/defaultEvents.js.map +1 -0
  11. package/dist/cjs/elements/form.cjs +8 -2
  12. package/dist/cjs/elements/window.cjs +7 -0
  13. package/dist/cjs/index.cjs +5 -0
  14. package/dist/cjs/reactComponents/Checkbox.cjs +11 -7
  15. package/dist/cjs/reactComponents/Honeypot.cjs +67 -0
  16. package/dist/cjs/reactComponents/TestModeBanner.cjs +47 -0
  17. package/dist/elements/form.d.ts +5 -0
  18. package/dist/elements/form.d.ts.map +1 -0
  19. package/dist/elements/form.js +8 -2
  20. package/dist/elements/form.js.map +1 -0
  21. package/dist/elements/window.d.ts +3 -0
  22. package/dist/elements/window.d.ts.map +1 -0
  23. package/dist/elements/window.js +8 -1
  24. package/dist/elements/window.js.map +1 -0
  25. package/dist/extensionLoader.d.ts +2 -0
  26. package/dist/extensionLoader.d.ts.map +1 -0
  27. package/dist/extensionLoader.js.map +1 -0
  28. package/dist/index.d.ts +11 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +6 -1
  31. package/dist/index.js.map +1 -0
  32. package/dist/providers.d.ts +4 -0
  33. package/dist/providers.d.ts.map +1 -0
  34. package/dist/providers.js.map +1 -0
  35. package/dist/reactComponents/Checkbox.d.ts +13 -0
  36. package/dist/reactComponents/Checkbox.d.ts.map +1 -0
  37. package/dist/reactComponents/Checkbox.js +11 -7
  38. package/dist/reactComponents/Checkbox.js.map +1 -0
  39. package/dist/reactComponents/Honeypot.d.ts +6 -0
  40. package/dist/reactComponents/Honeypot.d.ts.map +1 -0
  41. package/dist/reactComponents/Honeypot.js +67 -0
  42. package/dist/reactComponents/Honeypot.js.map +1 -0
  43. package/dist/reactComponents/Reload.d.ts +8 -0
  44. package/dist/reactComponents/Reload.d.ts.map +1 -0
  45. package/dist/reactComponents/Reload.js.map +1 -0
  46. package/dist/reactComponents/TestModeBanner.d.ts +7 -0
  47. package/dist/reactComponents/TestModeBanner.d.ts.map +1 -0
  48. package/dist/reactComponents/TestModeBanner.js +47 -0
  49. package/dist/reactComponents/TestModeBanner.js.map +1 -0
  50. package/dist/state/builder.d.ts +9 -0
  51. package/dist/state/builder.d.ts.map +1 -0
  52. package/dist/state/builder.js.map +1 -0
  53. package/dist/tests/defaultCallbacks.test.d.ts +2 -0
  54. package/dist/tests/defaultCallbacks.test.d.ts.map +1 -0
  55. package/dist/tests/defaultCallbacks.test.js +219 -0
  56. package/dist/tests/defaultCallbacks.test.js.map +1 -0
  57. package/dist/tests/defaultEvents.test.d.ts +2 -0
  58. package/dist/tests/defaultEvents.test.d.ts.map +1 -0
  59. package/dist/tests/defaultEvents.test.js +54 -0
  60. package/dist/tests/defaultEvents.test.js.map +1 -0
  61. package/dist/tests/extensionLoader.test.d.ts +2 -0
  62. package/dist/tests/extensionLoader.test.d.ts.map +1 -0
  63. package/dist/tests/extensionLoader.test.js +21 -0
  64. package/dist/tests/extensionLoader.test.js.map +1 -0
  65. package/dist/tests/form.test.d.ts +2 -0
  66. package/dist/tests/form.test.d.ts.map +1 -0
  67. package/dist/tests/form.test.js +98 -0
  68. package/dist/tests/form.test.js.map +1 -0
  69. package/dist/tests/providers.test.d.ts +2 -0
  70. package/dist/tests/providers.test.d.ts.map +1 -0
  71. package/dist/tests/providers.test.js +111 -0
  72. package/dist/tests/providers.test.js.map +1 -0
  73. package/dist/tests/state-builder.test.d.ts +2 -0
  74. package/dist/tests/state-builder.test.d.ts.map +1 -0
  75. package/dist/tests/state-builder.test.js +193 -0
  76. package/dist/tests/state-builder.test.js.map +1 -0
  77. package/dist/tests/window.test.d.ts +2 -0
  78. package/dist/tests/window.test.d.ts.map +1 -0
  79. package/dist/tests/window.test.js +80 -0
  80. package/dist/tests/window.test.js.map +1 -0
  81. package/package.json +13 -9
  82. package/src/callbacks/defaultCallbacks.ts +197 -0
  83. package/src/callbacks/defaultEvents.ts +21 -0
  84. package/src/elements/form.ts +41 -0
  85. package/src/elements/window.ts +39 -0
  86. package/src/extensionLoader.ts +17 -0
  87. package/src/index.ts +24 -0
  88. package/src/providers.ts +49 -0
  89. package/src/reactComponents/Checkbox.tsx +203 -0
  90. package/src/reactComponents/Honeypot.tsx +133 -0
  91. package/src/reactComponents/Reload.tsx +99 -0
  92. package/src/reactComponents/TestModeBanner.tsx +75 -0
  93. package/src/state/builder.ts +137 -0
  94. package/src/tests/defaultCallbacks.test.ts +372 -0
  95. package/src/tests/defaultEvents.test.ts +80 -0
  96. package/src/tests/extensionLoader.test.ts +41 -0
  97. package/src/tests/form.test.ts +154 -0
  98. package/src/tests/providers.test.ts +175 -0
  99. package/src/tests/state-builder.test.ts +264 -0
  100. package/src/tests/window.test.ts +137 -0
  101. package/tsconfig.cjs.json +32 -0
  102. package/tsconfig.json +33 -0
  103. package/tsconfig.tsbuildinfo +1 -0
  104. package/tsconfig.types.json +9 -0
  105. package/vite.cjs.config.ts +1 -1
  106. package/vite.esm.config.ts +1 -1
  107. package/vite.test.config.ts +1 -1
@@ -0,0 +1,154 @@
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 { ApiParams } from "@prosopo/types";
16
+ import { beforeEach, describe, expect, it } from "vitest";
17
+ import { getParentForm, removeProcaptchaResponse } from "../elements/form.js";
18
+
19
+ describe("elements/form", () => {
20
+ describe("getParentForm", () => {
21
+ beforeEach(() => {
22
+ document.body.innerHTML = "";
23
+ });
24
+
25
+ it("should find parent form for regular DOM element", () => {
26
+ const form = document.createElement("form");
27
+ const div = document.createElement("div");
28
+ const widget = document.createElement("div");
29
+
30
+ form.appendChild(div);
31
+ div.appendChild(widget);
32
+ document.body.appendChild(form);
33
+
34
+ const result = getParentForm(widget);
35
+ expect(result).toBe(form);
36
+ });
37
+
38
+ it("should return null if no parent form exists", () => {
39
+ const widget = document.createElement("div");
40
+ document.body.appendChild(widget);
41
+
42
+ const result = getParentForm(widget);
43
+ expect(result).toBeNull();
44
+ });
45
+
46
+ it("should find parent form for element in shadow DOM", () => {
47
+ const form = document.createElement("form");
48
+ const host = document.createElement("div");
49
+ const shadowRoot = host.attachShadow({ mode: "open" });
50
+ const widget = document.createElement("div");
51
+
52
+ shadowRoot.appendChild(widget);
53
+ form.appendChild(host);
54
+ document.body.appendChild(form);
55
+
56
+ const result = getParentForm(widget);
57
+ expect(result).toBe(form);
58
+ });
59
+
60
+ it("should handle nested forms and return closest one", () => {
61
+ const outerForm = document.createElement("form");
62
+ outerForm.id = "outer";
63
+ const innerForm = document.createElement("form");
64
+ innerForm.id = "inner";
65
+ const widget = document.createElement("div");
66
+
67
+ innerForm.appendChild(widget);
68
+ outerForm.appendChild(innerForm);
69
+ document.body.appendChild(outerForm);
70
+
71
+ const result = getParentForm(widget);
72
+ expect(result).toBe(innerForm);
73
+ });
74
+ });
75
+
76
+ describe("removeProcaptchaResponse", () => {
77
+ beforeEach(() => {
78
+ document.body.innerHTML = "";
79
+ });
80
+
81
+ it("should remove all procaptcha response elements", () => {
82
+ const input1 = document.createElement("input");
83
+ input1.name = ApiParams.procaptchaResponse;
84
+ input1.value = "token1";
85
+
86
+ const input2 = document.createElement("input");
87
+ input2.name = ApiParams.procaptchaResponse;
88
+ input2.value = "token2";
89
+
90
+ document.body.appendChild(input1);
91
+ document.body.appendChild(input2);
92
+
93
+ expect(
94
+ document.getElementsByName(ApiParams.procaptchaResponse).length,
95
+ ).toBe(2);
96
+
97
+ removeProcaptchaResponse();
98
+
99
+ expect(
100
+ document.getElementsByName(ApiParams.procaptchaResponse).length,
101
+ ).toBe(0);
102
+ });
103
+
104
+ it("should not remove other input elements", () => {
105
+ const procaptchaInput = document.createElement("input");
106
+ procaptchaInput.name = ApiParams.procaptchaResponse;
107
+
108
+ const otherInput = document.createElement("input");
109
+ otherInput.name = "other-field";
110
+
111
+ document.body.appendChild(procaptchaInput);
112
+ document.body.appendChild(otherInput);
113
+
114
+ removeProcaptchaResponse();
115
+
116
+ expect(
117
+ document.getElementsByName(ApiParams.procaptchaResponse).length,
118
+ ).toBe(0);
119
+ expect(document.getElementsByName("other-field").length).toBe(1);
120
+ });
121
+
122
+ it("should handle case when no procaptcha response elements exist", () => {
123
+ expect(() => removeProcaptchaResponse()).not.toThrow();
124
+ expect(
125
+ document.getElementsByName(ApiParams.procaptchaResponse).length,
126
+ ).toBe(0);
127
+ });
128
+
129
+ it("should remove elements from different parts of the DOM", () => {
130
+ const form1 = document.createElement("form");
131
+ const input1 = document.createElement("input");
132
+ input1.name = ApiParams.procaptchaResponse;
133
+ form1.appendChild(input1);
134
+
135
+ const form2 = document.createElement("form");
136
+ const input2 = document.createElement("input");
137
+ input2.name = ApiParams.procaptchaResponse;
138
+ form2.appendChild(input2);
139
+
140
+ document.body.appendChild(form1);
141
+ document.body.appendChild(form2);
142
+
143
+ expect(
144
+ document.getElementsByName(ApiParams.procaptchaResponse).length,
145
+ ).toBe(2);
146
+
147
+ removeProcaptchaResponse();
148
+
149
+ expect(
150
+ document.getElementsByName(ApiParams.procaptchaResponse).length,
151
+ ).toBe(0);
152
+ });
153
+ });
154
+ });
@@ -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 { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
16
+ import {
17
+ getProcaptchaRandomActiveProvider,
18
+ providerRetry,
19
+ } from "../providers.js";
20
+
21
+ // Mock the load-balancer module
22
+ vi.mock("@prosopo/load-balancer", () => ({
23
+ getRandomActiveProvider: vi.fn(),
24
+ }));
25
+
26
+ describe("providers", () => {
27
+ describe("getProcaptchaRandomActiveProvider", () => {
28
+ // biome-ignore lint/suspicious/noExplicitAny: Store original crypto function
29
+ let originalGetRandomValues: any;
30
+
31
+ beforeEach(() => {
32
+ originalGetRandomValues = global.window.crypto.getRandomValues.bind(
33
+ global.window.crypto,
34
+ );
35
+ });
36
+
37
+ afterEach(() => {
38
+ global.window.crypto.getRandomValues = originalGetRandomValues;
39
+ });
40
+
41
+ it("should generate random values and call getRandomActiveProvider", async () => {
42
+ // Mock window.crypto.getRandomValues
43
+ const mockRandomValues = new Uint8Array([
44
+ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100,
45
+ ]);
46
+ const mockGetRandomValues = vi.fn(() => mockRandomValues);
47
+ // biome-ignore lint/suspicious/noExplicitAny: Mock crypto API
48
+ global.window.crypto.getRandomValues = mockGetRandomValues as any;
49
+
50
+ // Mock the getRandomActiveProvider import
51
+ const { getRandomActiveProvider } = await import(
52
+ "@prosopo/load-balancer"
53
+ );
54
+ vi.mocked(getRandomActiveProvider).mockResolvedValue({
55
+ providerUrl: "https://test-provider.com",
56
+ // biome-ignore lint/suspicious/noExplicitAny: Mock return type
57
+ } as any);
58
+
59
+ const result = await getProcaptchaRandomActiveProvider("development");
60
+
61
+ expect(mockGetRandomValues).toHaveBeenCalledWith(expect.any(Uint8Array));
62
+ expect(getRandomActiveProvider).toHaveBeenCalledWith("development", 550); // sum of mockRandomValues
63
+ expect(result).toEqual({ providerUrl: "https://test-provider.com" });
64
+ });
65
+
66
+ it("should use different random values on each call", async () => {
67
+ let callCount = 0;
68
+ const mockGetRandomValues = vi.fn((arr: Uint8Array) => {
69
+ callCount++;
70
+ arr.fill(callCount);
71
+ return arr;
72
+ });
73
+ // biome-ignore lint/suspicious/noExplicitAny: Mock crypto API
74
+ global.window.crypto.getRandomValues = mockGetRandomValues as any;
75
+
76
+ const { getRandomActiveProvider } = await import(
77
+ "@prosopo/load-balancer"
78
+ );
79
+ // biome-ignore lint/suspicious/noExplicitAny: Mock return type
80
+ vi.mocked(getRandomActiveProvider).mockResolvedValue({} as any);
81
+
82
+ await getProcaptchaRandomActiveProvider("development");
83
+ await getProcaptchaRandomActiveProvider("development");
84
+
85
+ expect(mockGetRandomValues).toHaveBeenCalledTimes(2);
86
+ });
87
+ });
88
+
89
+ describe("providerRetry", () => {
90
+ it("should successfully execute currentFn without retrying", async () => {
91
+ const currentFn = vi.fn().mockResolvedValue(undefined);
92
+ const retryFn = vi.fn();
93
+ const stateReset = vi.fn();
94
+
95
+ await providerRetry(currentFn, retryFn, stateReset, 0, 3);
96
+
97
+ expect(currentFn).toHaveBeenCalledTimes(1);
98
+ expect(retryFn).not.toHaveBeenCalled();
99
+ expect(stateReset).not.toHaveBeenCalled();
100
+ });
101
+
102
+ it("should call retryFn when currentFn fails and attempts are below max", async () => {
103
+ const error = new Error("Provider failed");
104
+ const currentFn = vi.fn().mockRejectedValue(error);
105
+ const retryFn = vi.fn().mockResolvedValue(undefined);
106
+ const stateReset = vi.fn();
107
+
108
+ await providerRetry(currentFn, retryFn, stateReset, 1, 3);
109
+
110
+ expect(currentFn).toHaveBeenCalledTimes(1);
111
+ expect(stateReset).toHaveBeenCalledTimes(1);
112
+ expect(retryFn).toHaveBeenCalledTimes(1);
113
+ });
114
+
115
+ it("should call stateReset and not retry when max retries reached", async () => {
116
+ const error = new Error("Provider failed");
117
+ const currentFn = vi.fn().mockRejectedValue(error);
118
+ const retryFn = vi.fn();
119
+ const stateReset = vi.fn();
120
+ const consoleErrorSpy = vi
121
+ .spyOn(console, "error")
122
+ .mockImplementation(() => {});
123
+
124
+ await providerRetry(currentFn, retryFn, stateReset, 3, 3);
125
+
126
+ expect(currentFn).toHaveBeenCalledTimes(1);
127
+ expect(stateReset).toHaveBeenCalledTimes(1);
128
+ expect(retryFn).not.toHaveBeenCalled();
129
+ expect(consoleErrorSpy).toHaveBeenCalledWith(error);
130
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
131
+ "Max retries (3 of 3) reached, aborting",
132
+ );
133
+
134
+ consoleErrorSpy.mockRestore();
135
+ });
136
+
137
+ it("should call stateReset and not retry when attempts exceed max", async () => {
138
+ const error = new Error("Provider failed");
139
+ const currentFn = vi.fn().mockRejectedValue(error);
140
+ const retryFn = vi.fn();
141
+ const stateReset = vi.fn();
142
+ const consoleErrorSpy = vi
143
+ .spyOn(console, "error")
144
+ .mockImplementation(() => {});
145
+
146
+ await providerRetry(currentFn, retryFn, stateReset, 5, 3);
147
+
148
+ expect(currentFn).toHaveBeenCalledTimes(1);
149
+ expect(stateReset).toHaveBeenCalledTimes(1);
150
+ expect(retryFn).not.toHaveBeenCalled();
151
+
152
+ consoleErrorSpy.mockRestore();
153
+ });
154
+
155
+ it("should reset state even when retryFn fails", async () => {
156
+ const error = new Error("Provider failed");
157
+ const currentFn = vi.fn().mockRejectedValue(error);
158
+ const retryFn = vi.fn().mockRejectedValue(new Error("Retry failed"));
159
+ const stateReset = vi.fn();
160
+ const consoleErrorSpy = vi
161
+ .spyOn(console, "error")
162
+ .mockImplementation(() => {});
163
+
164
+ await expect(
165
+ providerRetry(currentFn, retryFn, stateReset, 1, 3),
166
+ ).rejects.toThrow("Retry failed");
167
+
168
+ expect(currentFn).toHaveBeenCalledTimes(1);
169
+ expect(stateReset).toHaveBeenCalledTimes(1);
170
+ expect(retryFn).toHaveBeenCalledTimes(1);
171
+
172
+ consoleErrorSpy.mockRestore();
173
+ });
174
+ });
175
+ });
@@ -0,0 +1,264 @@
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 { ProcaptchaState } from "@prosopo/types";
16
+ import { describe, expect, it, vi } from "vitest";
17
+ import { buildUpdateState, useProcaptcha } from "../state/builder.js";
18
+
19
+ describe("state/builder", () => {
20
+ describe("buildUpdateState", () => {
21
+ it("should mutate state and call onStateUpdate with partial state", () => {
22
+ const state: ProcaptchaState = {
23
+ isHuman: false,
24
+ index: 0,
25
+ solutions: [],
26
+ captchaApi: undefined,
27
+ showModal: false,
28
+ challenge: undefined,
29
+ loading: false,
30
+ account: undefined,
31
+ dappAccount: undefined,
32
+ submission: undefined,
33
+ timeout: undefined,
34
+ successfullChallengeTimeout: undefined,
35
+ sendData: false,
36
+ attemptCount: 0,
37
+ error: undefined,
38
+ sessionId: undefined,
39
+ };
40
+
41
+ const onStateUpdate = vi.fn();
42
+ const updateState = buildUpdateState(state, onStateUpdate);
43
+
44
+ const partialState = { isHuman: true, attemptCount: 1 };
45
+ updateState(partialState);
46
+
47
+ expect(state.isHuman).toBe(true);
48
+ expect(state.attemptCount).toBe(1);
49
+ expect(onStateUpdate).toHaveBeenCalledWith(partialState);
50
+ });
51
+
52
+ it("should handle multiple property updates in order", () => {
53
+ const state: ProcaptchaState = {
54
+ isHuman: false,
55
+ index: 0,
56
+ solutions: [],
57
+ captchaApi: undefined,
58
+ showModal: false,
59
+ challenge: undefined,
60
+ loading: false,
61
+ account: undefined,
62
+ dappAccount: undefined,
63
+ submission: undefined,
64
+ timeout: undefined,
65
+ successfullChallengeTimeout: undefined,
66
+ sendData: false,
67
+ attemptCount: 0,
68
+ error: undefined,
69
+ sessionId: undefined,
70
+ };
71
+
72
+ const onStateUpdate = vi.fn();
73
+ const updateState = buildUpdateState(state, onStateUpdate);
74
+
75
+ updateState({ loading: true, index: 5, showModal: true });
76
+
77
+ expect(state.loading).toBe(true);
78
+ expect(state.index).toBe(5);
79
+ expect(state.showModal).toBe(true);
80
+ });
81
+
82
+ it("should update error state correctly", () => {
83
+ const state: ProcaptchaState = {
84
+ isHuman: false,
85
+ index: 0,
86
+ solutions: [],
87
+ captchaApi: undefined,
88
+ showModal: false,
89
+ challenge: undefined,
90
+ loading: false,
91
+ account: undefined,
92
+ dappAccount: undefined,
93
+ submission: undefined,
94
+ timeout: undefined,
95
+ successfullChallengeTimeout: undefined,
96
+ sendData: false,
97
+ attemptCount: 0,
98
+ error: undefined,
99
+ sessionId: undefined,
100
+ };
101
+
102
+ const onStateUpdate = vi.fn();
103
+ const updateState = buildUpdateState(state, onStateUpdate);
104
+
105
+ const errorObj = { message: "Test error", key: "test" };
106
+ updateState({ error: errorObj });
107
+
108
+ expect(state.error).toEqual(errorObj);
109
+ expect(onStateUpdate).toHaveBeenCalledWith({ error: errorObj });
110
+ });
111
+ });
112
+
113
+ describe("useProcaptcha", () => {
114
+ it("should initialize with default state values", () => {
115
+ const useState = vi.fn((defaultValue) => [defaultValue, vi.fn()]);
116
+ const useRef = vi.fn((defaultValue) => ({ current: defaultValue }));
117
+
118
+ // biome-ignore lint/suspicious/noExplicitAny: Mock useState/useRef functions
119
+ const [state, updateFn] = useProcaptcha(useState as any, useRef as any);
120
+
121
+ expect(state.isHuman).toBe(false);
122
+ expect(state.index).toBe(0);
123
+ expect(state.solutions).toEqual([]);
124
+ expect(state.showModal).toBe(false);
125
+ expect(state.loading).toBe(false);
126
+ expect(state.sendData).toBe(false);
127
+ expect(state.attemptCount).toBe(0);
128
+ expect(state.account).toBeUndefined();
129
+ expect(state.captchaApi).toBeUndefined();
130
+ expect(state.challenge).toBeUndefined();
131
+ expect(state.error).toBeUndefined();
132
+ expect(typeof updateFn).toBe("function");
133
+ });
134
+
135
+ it("should call appropriate setters when updating state", () => {
136
+ // biome-ignore lint/suspicious/noExplicitAny: Mock setter functions
137
+ const setters: Record<string, any> = {};
138
+
139
+ const useState = vi.fn((defaultValue) => {
140
+ // Create a unique key based on the type and value to track different state variables
141
+ const key =
142
+ typeof defaultValue === "boolean"
143
+ ? `boolean-${defaultValue}`
144
+ : typeof defaultValue === "number"
145
+ ? `number-${defaultValue}`
146
+ : Array.isArray(defaultValue)
147
+ ? "array"
148
+ : "other";
149
+
150
+ if (!setters[key]) {
151
+ setters[key] = vi.fn();
152
+ }
153
+ return [defaultValue, setters[key]];
154
+ });
155
+
156
+ const useRef = vi.fn((defaultValue) => ({ current: defaultValue }));
157
+
158
+ // biome-ignore lint/suspicious/noExplicitAny: Mock useState/useRef functions
159
+ const [state, updateFn] = useProcaptcha(useState as any, useRef as any);
160
+
161
+ updateFn({
162
+ isHuman: true,
163
+ index: 5,
164
+ loading: true,
165
+ attemptCount: 3,
166
+ });
167
+
168
+ // Check that setters were called
169
+ // isHuman setter (boolean-false)
170
+ expect(setters["boolean-false"]).toHaveBeenCalledWith(true);
171
+ // index and attemptCount setters (both number-0)
172
+ expect(setters["number-0"]).toHaveBeenCalledWith(5);
173
+ expect(setters["number-0"]).toHaveBeenCalledWith(3);
174
+ });
175
+
176
+ it("should handle solutions array correctly with slice", () => {
177
+ const setSolutions = vi.fn();
178
+ // biome-ignore lint/suspicious/noExplicitAny: Mock solutions array
179
+ let solutionsValue: any[] = [];
180
+
181
+ const useState = vi.fn((defaultValue) => {
182
+ if (Array.isArray(defaultValue)) {
183
+ return [
184
+ solutionsValue,
185
+ // biome-ignore lint/suspicious/noExplicitAny: Mock setter function
186
+ (newValue: any) => {
187
+ solutionsValue = newValue;
188
+ setSolutions(newValue);
189
+ },
190
+ ];
191
+ }
192
+ return [defaultValue, vi.fn()];
193
+ });
194
+
195
+ const useRef = vi.fn((defaultValue) => ({ current: defaultValue }));
196
+
197
+ // biome-ignore lint/suspicious/noExplicitAny: Mock useState/useRef functions
198
+ const [state, updateFn] = useProcaptcha(useState as any, useRef as any);
199
+
200
+ const newSolutions: [string, number, number][][] = [[["test", 1, 2]]];
201
+ updateFn({ solutions: newSolutions });
202
+
203
+ expect(setSolutions).toHaveBeenCalled();
204
+ // Verify that slice was called by checking that we got a new array reference
205
+ const callArg = setSolutions.mock.calls[0]?.[0];
206
+ expect(callArg).toBeDefined();
207
+ expect(callArg).not.toBe(newSolutions);
208
+ expect(callArg).toEqual(newSolutions);
209
+ });
210
+
211
+ it("should not call setters for undefined values in partial update", () => {
212
+ const setIsHuman = vi.fn();
213
+ const setIndex = vi.fn();
214
+
215
+ // biome-ignore lint/suspicious/noExplicitAny: Mock setter tracking
216
+ let isHumanSetter: any;
217
+ // biome-ignore lint/suspicious/noExplicitAny: Mock setter tracking
218
+ let indexSetter: any;
219
+
220
+ const useState = vi.fn((defaultValue) => {
221
+ if (defaultValue === false) {
222
+ isHumanSetter = setIsHuman;
223
+ return [false, setIsHuman];
224
+ }
225
+ if (defaultValue === 0) {
226
+ indexSetter = setIndex;
227
+ return [0, setIndex];
228
+ }
229
+ return [defaultValue, vi.fn()];
230
+ });
231
+
232
+ const useRef = vi.fn((defaultValue) => ({ current: defaultValue }));
233
+
234
+ // biome-ignore lint/suspicious/noExplicitAny: Mock useState/useRef functions
235
+ const [state, updateFn] = useProcaptcha(useState as any, useRef as any);
236
+
237
+ updateFn({ isHuman: true });
238
+
239
+ expect(setIsHuman).toHaveBeenCalledWith(true);
240
+ expect(setIndex).not.toHaveBeenCalled();
241
+ });
242
+
243
+ it("should handle error state update", () => {
244
+ const setError = vi.fn();
245
+
246
+ const useState = vi.fn((defaultValue) => {
247
+ if (defaultValue === undefined && setError.mock.calls.length === 0) {
248
+ return [undefined, setError];
249
+ }
250
+ return [defaultValue, vi.fn()];
251
+ });
252
+
253
+ const useRef = vi.fn((defaultValue) => ({ current: defaultValue }));
254
+
255
+ // biome-ignore lint/suspicious/noExplicitAny: Mock useState/useRef functions
256
+ const [state, updateFn] = useProcaptcha(useState as any, useRef as any);
257
+
258
+ const errorObj = { message: "Test error", key: "testKey" };
259
+ updateFn({ error: errorObj });
260
+
261
+ expect(setError).toHaveBeenCalledWith(errorObj);
262
+ });
263
+ });
264
+ });