@prosopo/procaptcha-puzzle 2.13.3 → 2.14.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 +6 -6
- package/.turbo/turbo-build$colon$tsc.log +19 -19
- package/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +41 -0
- package/dist/cjs/components/procaptchaPuzzle.cjs +22 -0
- package/dist/cjs/components/procaptchaWidget.cjs +225 -0
- package/dist/cjs/components/puzzleCanvas.cjs +450 -0
- package/dist/cjs/index.cjs +7 -2
- package/dist/components/procaptchaPuzzle.d.ts +7 -0
- package/dist/components/procaptchaPuzzle.d.ts.map +1 -0
- package/dist/components/procaptchaPuzzle.js +21 -0
- package/dist/components/procaptchaPuzzle.js.map +1 -0
- package/dist/components/procaptchaWidget.d.ts +7 -0
- package/dist/components/procaptchaWidget.d.ts.map +1 -0
- package/dist/components/procaptchaWidget.js +225 -0
- package/dist/components/procaptchaWidget.js.map +1 -0
- package/dist/components/{PuzzleCanvas.d.ts → puzzleCanvas.d.ts} +6 -4
- package/dist/components/puzzleCanvas.d.ts.map +1 -0
- package/dist/components/puzzleCanvas.js +450 -0
- package/dist/components/puzzleCanvas.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/tests/domHarness.d.ts +20 -0
- package/dist/tests/domHarness.d.ts.map +1 -0
- package/dist/tests/domHarness.js +56 -0
- package/dist/tests/domHarness.js.map +1 -0
- package/dist/tests/procaptchaPuzzle.test-d.js +13 -10
- package/dist/tests/procaptchaPuzzle.test-d.js.map +1 -1
- package/dist/tests/procaptchaPuzzle.unit.test.js +44 -33
- package/dist/tests/procaptchaPuzzle.unit.test.js.map +1 -1
- package/dist/tests/procaptchaWidget.unit.test.js +108 -175
- package/dist/tests/procaptchaWidget.unit.test.js.map +1 -1
- package/dist/tests/puzzleCanvas.unit.test.js +47 -80
- package/dist/tests/puzzleCanvas.unit.test.js.map +1 -1
- package/package.json +8 -12
- package/src/components/procaptchaPuzzle.ts +54 -0
- package/src/components/procaptchaWidget.ts +392 -0
- package/src/components/puzzleCanvas.ts +653 -0
- package/src/index.ts +3 -2
- package/src/tests/domHarness.ts +122 -0
- package/src/tests/procaptchaPuzzle.test-d.ts +33 -17
- package/src/tests/procaptchaPuzzle.unit.test.ts +59 -44
- package/src/tests/procaptchaWidget.unit.test.ts +145 -258
- package/src/tests/puzzleCanvas.unit.test.ts +76 -120
- package/tsconfig.cjs.json +1 -2
- package/tsconfig.json +1 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/vite.test.config.ts +3 -4
- package/dist/cjs/components/ProcaptchaPuzzle.cjs +0 -7
- package/dist/cjs/components/ProcaptchaWidget.cjs +0 -206
- package/dist/cjs/components/PuzzleCanvas.cjs +0 -476
- package/dist/components/ProcaptchaPuzzle.d.ts +0 -3
- package/dist/components/ProcaptchaPuzzle.d.ts.map +0 -1
- package/dist/components/ProcaptchaPuzzle.js +0 -7
- package/dist/components/ProcaptchaPuzzle.js.map +0 -1
- package/dist/components/ProcaptchaWidget.d.ts +0 -4
- package/dist/components/ProcaptchaWidget.d.ts.map +0 -1
- package/dist/components/ProcaptchaWidget.js +0 -207
- package/dist/components/ProcaptchaWidget.js.map +0 -1
- package/dist/components/PuzzleCanvas.d.ts.map +0 -1
- package/dist/components/PuzzleCanvas.js +0 -476
- package/dist/components/PuzzleCanvas.js.map +0 -1
- package/dist/tests/setup.d.ts +0 -5
- package/dist/tests/setup.d.ts.map +0 -1
- package/dist/tests/setup.js +0 -3
- package/dist/tests/setup.js.map +0 -1
- package/src/components/ProcaptchaPuzzle.tsx +0 -36
- package/src/components/ProcaptchaWidget.tsx +0 -320
- package/src/components/PuzzleCanvas.tsx +0 -638
- package/src/tests/setup.ts +0 -26
|
@@ -1,22 +1,27 @@
|
|
|
1
|
-
import { act, createElement } from "react";
|
|
2
|
-
import { createRoot } from "react-dom/client";
|
|
3
1
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
4
2
|
import * as entrypoint from "../index.js";
|
|
3
|
+
import { mount, settle } from "./domHarness.js";
|
|
5
4
|
import { config } from "./managerHarness.js";
|
|
6
5
|
const mocks = vi.hoisted(() => ({
|
|
7
6
|
received: [],
|
|
7
|
+
destroyed: { count: 0 },
|
|
8
8
|
}));
|
|
9
|
-
vi.mock("../components/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
vi.mock("../components/procaptchaWidget.js", () => ({
|
|
10
|
+
mountProcaptchaPuzzleWidget: (container, props) => {
|
|
11
|
+
mocks.received.push(props);
|
|
12
|
+
const element = document.createElement("div");
|
|
13
|
+
element.setAttribute("data-testid", "widget");
|
|
14
|
+
container.appendChild(element);
|
|
15
|
+
return {
|
|
16
|
+
destroy: () => {
|
|
17
|
+
element.remove();
|
|
18
|
+
mocks.destroyed.count += 1;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
let mounted;
|
|
24
|
+
let widget;
|
|
20
25
|
const props = (overrides = {}) => ({
|
|
21
26
|
config: config(),
|
|
22
27
|
callbacks: {},
|
|
@@ -24,28 +29,24 @@ const props = (overrides = {}) => ({
|
|
|
24
29
|
...overrides,
|
|
25
30
|
});
|
|
26
31
|
const render = async (widgetProps) => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
});
|
|
32
|
+
widget = entrypoint.mountProcaptchaPuzzle(mounted.container, widgetProps);
|
|
33
|
+
await settle();
|
|
30
34
|
};
|
|
31
35
|
beforeEach(() => {
|
|
32
36
|
mocks.received.length = 0;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
root = createRoot(container);
|
|
37
|
-
});
|
|
37
|
+
mocks.destroyed.count = 0;
|
|
38
|
+
widget = undefined;
|
|
39
|
+
mounted = mount();
|
|
38
40
|
});
|
|
39
41
|
afterEach(() => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
container.remove();
|
|
42
|
+
widget?.destroy();
|
|
43
|
+
widget = undefined;
|
|
44
|
+
mounted.unmount();
|
|
44
45
|
});
|
|
45
|
-
describe("
|
|
46
|
+
describe("mountProcaptchaPuzzle", () => {
|
|
46
47
|
test("renders the widget once it has loaded", async () => {
|
|
47
48
|
await render(props());
|
|
48
|
-
expect(container.querySelector('[data-testid="widget"]')).not.toBeNull();
|
|
49
|
+
expect(mounted.container.querySelector('[data-testid="widget"]')).not.toBeNull();
|
|
49
50
|
});
|
|
50
51
|
test("passes on the props the widget needs but never names", async () => {
|
|
51
52
|
const onSessionInvalidated = vi.fn();
|
|
@@ -66,16 +67,26 @@ describe("ProcaptchaPuzzle", () => {
|
|
|
66
67
|
expect(mocks.received[0]?.callbacks).toBe(callbacks);
|
|
67
68
|
expect(mocks.received[0]?.config).toEqual(config());
|
|
68
69
|
});
|
|
69
|
-
test("
|
|
70
|
+
test("tears the loaded widget down", async () => {
|
|
70
71
|
await render(props());
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
widget?.destroy();
|
|
73
|
+
widget = undefined;
|
|
74
|
+
expect(mocks.destroyed.count).toBe(1);
|
|
75
|
+
expect(mounted.container.querySelector('[data-testid="widget"]')).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
test("cancels a mount that is still loading", async () => {
|
|
78
|
+
widget = entrypoint.mountProcaptchaPuzzle(mounted.container, props());
|
|
79
|
+
widget.destroy();
|
|
80
|
+
widget = undefined;
|
|
81
|
+
await settle();
|
|
82
|
+
expect(mocks.received).toHaveLength(0);
|
|
73
83
|
});
|
|
74
84
|
});
|
|
75
85
|
describe("the package entrypoint", () => {
|
|
76
|
-
test("exports the
|
|
77
|
-
expect(typeof entrypoint.
|
|
78
|
-
expect(
|
|
86
|
+
test("exports the mount functions a consumer needs", () => {
|
|
87
|
+
expect(typeof entrypoint.mountProcaptchaPuzzle).toBe("function");
|
|
88
|
+
expect(typeof entrypoint.loadProcaptchaPuzzle).toBe("function");
|
|
89
|
+
expect(typeof entrypoint.mountProcaptchaPuzzleWidget).toBe("function");
|
|
79
90
|
});
|
|
80
91
|
});
|
|
81
92
|
//# sourceMappingURL=procaptchaPuzzle.unit.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"procaptchaPuzzle.unit.test.js","sourceRoot":"","sources":["../../src/tests/procaptchaPuzzle.unit.test.ts"],"names":[],"mappings":"AAgBA,OAAO,
|
|
1
|
+
{"version":3,"file":"procaptchaPuzzle.unit.test.js","sourceRoot":"","sources":["../../src/tests/procaptchaPuzzle.unit.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { ModeEnum, } from "@prosopo/types";
|
|
2
|
-
import { act, createElement } from "react";
|
|
3
|
-
import { createRoot } from "react-dom/client";
|
|
4
2
|
import { afterEach, beforeEach, describe, expect, test, vi, } from "vitest";
|
|
5
|
-
import
|
|
3
|
+
import { mountProcaptchaPuzzleWidget, } from "../components/procaptchaWidget.js";
|
|
4
|
+
import { fire, mount, settle } from "./domHarness.js";
|
|
6
5
|
import { challengeResponse, config, frictionless } from "./managerHarness.js";
|
|
7
6
|
const mocks = vi.hoisted(() => {
|
|
8
7
|
const start = vi.fn();
|
|
@@ -10,9 +9,9 @@ const mocks = vi.hoisted(() => {
|
|
|
10
9
|
const resetState = vi.fn();
|
|
11
10
|
const constructions = [];
|
|
12
11
|
const loadI18next = vi.fn();
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
const canvasProps = {
|
|
13
|
+
current: undefined,
|
|
14
|
+
};
|
|
16
15
|
const translationsReady = { current: true };
|
|
17
16
|
return {
|
|
18
17
|
translationsReady,
|
|
@@ -21,9 +20,7 @@ const mocks = vi.hoisted(() => {
|
|
|
21
20
|
resetState,
|
|
22
21
|
constructions,
|
|
23
22
|
loadI18next,
|
|
24
|
-
checkboxProps,
|
|
25
23
|
canvasProps,
|
|
26
|
-
honeypotQuestions,
|
|
27
24
|
};
|
|
28
25
|
});
|
|
29
26
|
vi.mock("../services/Manager.js", () => ({
|
|
@@ -36,47 +33,38 @@ vi.mock("../services/Manager.js", () => ({
|
|
|
36
33
|
};
|
|
37
34
|
},
|
|
38
35
|
}));
|
|
39
|
-
vi.mock("../components/
|
|
40
|
-
|
|
41
|
-
const PuzzleCanvas = (canvasProps) => {
|
|
36
|
+
vi.mock("../components/puzzleCanvas.js", () => ({
|
|
37
|
+
mountPuzzleCanvas: (canvasProps) => {
|
|
42
38
|
mocks.canvasProps.current = canvasProps;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"data-error": checkboxProps.error,
|
|
58
|
-
"data-loading": String(checkboxProps.loading),
|
|
59
|
-
});
|
|
60
|
-
};
|
|
61
|
-
const Honeypot = forwardRef(({ encodedQuestion }, ref) => {
|
|
62
|
-
mocks.honeypotQuestions.push(encodedQuestion);
|
|
63
|
-
return create("input", { type: "text", ref, name: "honeypot" });
|
|
64
|
-
});
|
|
65
|
-
return { ...actual, Checkbox, Honeypot };
|
|
66
|
-
});
|
|
39
|
+
const element = document.createElement("div");
|
|
40
|
+
element.setAttribute("data-cy", "canvas-stub");
|
|
41
|
+
document.body.appendChild(element);
|
|
42
|
+
return {
|
|
43
|
+
update: (next) => {
|
|
44
|
+
mocks.canvasProps.current = next;
|
|
45
|
+
},
|
|
46
|
+
destroy: () => {
|
|
47
|
+
element.remove();
|
|
48
|
+
mocks.canvasProps.current = undefined;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
}));
|
|
67
53
|
vi.mock("@prosopo/locale", async (importOriginal) => {
|
|
68
54
|
const actual = await importOriginal();
|
|
69
55
|
return {
|
|
70
56
|
...actual,
|
|
71
57
|
loadI18next: mocks.loadI18next,
|
|
72
|
-
|
|
58
|
+
createTranslator: () => ({
|
|
73
59
|
t: (key) => key,
|
|
74
|
-
|
|
60
|
+
isReady: () => mocks.translationsReady.current,
|
|
61
|
+
subscribe: () => () => undefined,
|
|
62
|
+
i18n: undefined,
|
|
75
63
|
}),
|
|
76
64
|
};
|
|
77
65
|
});
|
|
78
|
-
let
|
|
79
|
-
let
|
|
66
|
+
let mounted;
|
|
67
|
+
let widget;
|
|
80
68
|
const i18nStub = (language, changeLanguage) => ({ language, changeLanguage });
|
|
81
69
|
const props = (overrides = {}) => ({
|
|
82
70
|
config: config(),
|
|
@@ -85,69 +73,53 @@ const props = (overrides = {}) => ({
|
|
|
85
73
|
...overrides,
|
|
86
74
|
});
|
|
87
75
|
const render = (widgetProps) => {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
76
|
+
widget = mountProcaptchaPuzzleWidget(mounted.container, widgetProps);
|
|
77
|
+
};
|
|
78
|
+
const destroy = () => {
|
|
79
|
+
widget?.destroy();
|
|
80
|
+
widget = undefined;
|
|
91
81
|
};
|
|
92
82
|
beforeEach(() => {
|
|
93
83
|
vi.clearAllMocks();
|
|
94
84
|
mocks.constructions.length = 0;
|
|
95
|
-
mocks.honeypotQuestions.length = 0;
|
|
96
85
|
mocks.translationsReady.current = true;
|
|
97
|
-
mocks.checkboxProps.current = undefined;
|
|
98
86
|
mocks.canvasProps.current = undefined;
|
|
99
87
|
mocks.start.mockResolvedValue(challengeResponse());
|
|
100
88
|
mocks.submitSolution.mockResolvedValue(true);
|
|
101
89
|
mocks.loadI18next.mockResolvedValue(undefined);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
act(() => {
|
|
105
|
-
root = createRoot(container);
|
|
106
|
-
});
|
|
90
|
+
widget = undefined;
|
|
91
|
+
mounted = mount();
|
|
107
92
|
});
|
|
108
93
|
afterEach(() => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
});
|
|
112
|
-
container.remove();
|
|
94
|
+
destroy();
|
|
95
|
+
mounted.unmount();
|
|
113
96
|
vi.useRealTimers();
|
|
114
97
|
vi.restoreAllMocks();
|
|
115
98
|
});
|
|
116
99
|
const checkbox = () => {
|
|
117
|
-
const element = container.querySelector('input[type="checkbox"]');
|
|
100
|
+
const element = mounted.container.querySelector('input[type="checkbox"]');
|
|
118
101
|
if (!element)
|
|
119
102
|
throw new Error("expected a checkbox to be rendered");
|
|
120
103
|
return element;
|
|
121
104
|
};
|
|
122
|
-
const canvas = () =>
|
|
105
|
+
const canvas = () => document.body.querySelector('[data-cy="canvas-stub"]');
|
|
106
|
+
const spinner = () => mounted.container.querySelector('[role="status"]');
|
|
123
107
|
const honeypotInput = () => {
|
|
124
|
-
const element =
|
|
108
|
+
const element = document.querySelector('input[name="email_confirm"]');
|
|
125
109
|
if (!element)
|
|
126
110
|
throw new Error("expected a honeypot to be rendered");
|
|
127
111
|
return element;
|
|
128
112
|
};
|
|
129
113
|
const click = async (options = {}) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
clientX: options.clientX ?? 0,
|
|
133
|
-
clientY: options.clientY ?? 0,
|
|
134
|
-
...(options.touches ? { touches: options.touches } : {}),
|
|
135
|
-
};
|
|
136
|
-
await act(async () => {
|
|
137
|
-
await mocks.checkboxProps.current?.onChange({ nativeEvent });
|
|
138
|
-
});
|
|
114
|
+
fire(checkbox(), "click", options);
|
|
115
|
+
await settle();
|
|
139
116
|
};
|
|
140
117
|
const clickWithoutWaiting = () => {
|
|
141
|
-
|
|
142
|
-
void mocks.checkboxProps.current?.onChange({
|
|
143
|
-
nativeEvent: { isTrusted: true, clientX: 0, clientY: 0 },
|
|
144
|
-
});
|
|
145
|
-
});
|
|
118
|
+
fire(checkbox(), "click");
|
|
146
119
|
};
|
|
147
120
|
const complete = async (x = 200, y = 80, events = [{ x: 200, y: 80, t: 5 }]) => {
|
|
148
|
-
await
|
|
149
|
-
|
|
150
|
-
});
|
|
121
|
+
await mocks.canvasProps.current?.onComplete(x, y, events);
|
|
122
|
+
await settle();
|
|
151
123
|
};
|
|
152
124
|
describe("what the widget renders", () => {
|
|
153
125
|
test("a visible widget shows a checkbox", () => {
|
|
@@ -156,23 +128,23 @@ describe("what the widget renders", () => {
|
|
|
156
128
|
});
|
|
157
129
|
test("an invisible widget shows no checkbox at all", () => {
|
|
158
130
|
render(props({ config: config({ mode: ModeEnum.invisible }) }));
|
|
159
|
-
expect(container.querySelector('input[type="checkbox"]')).toBeNull();
|
|
131
|
+
expect(mounted.container.querySelector('input[type="checkbox"]')).toBeNull();
|
|
160
132
|
});
|
|
161
133
|
test("no puzzle is shown until a challenge has been fetched", () => {
|
|
162
134
|
render(props());
|
|
163
135
|
expect(canvas()).toBeNull();
|
|
164
136
|
});
|
|
165
137
|
test("a session with a honeypot question renders the honeypot", () => {
|
|
166
|
-
render(props({ frictionlessState: frictionless({ hp: "question" }) }));
|
|
138
|
+
render(props({ frictionlessState: frictionless({ hp: btoa("question") }) }));
|
|
167
139
|
expect(honeypotInput()).toBeDefined();
|
|
168
|
-
expect(
|
|
140
|
+
expect(document.body.textContent).toContain("question");
|
|
169
141
|
});
|
|
170
142
|
test("a session without one renders no bait at all", () => {
|
|
171
143
|
render(props({ frictionlessState: frictionless() }));
|
|
172
|
-
expect(
|
|
144
|
+
expect(document.querySelector('input[name="email_confirm"]')).toBeNull();
|
|
173
145
|
});
|
|
174
146
|
test("the manager can read the honeypot input the widget rendered", () => {
|
|
175
|
-
render(props({ frictionlessState: frictionless({ hp: "question" }) }));
|
|
147
|
+
render(props({ frictionlessState: frictionless({ hp: btoa("question") }) }));
|
|
176
148
|
honeypotInput().value = "bot@example.com";
|
|
177
149
|
expect(mocks.constructions[0]?.getHoneypotValue?.()).toBe("bot@example.com");
|
|
178
150
|
});
|
|
@@ -182,25 +154,17 @@ describe("what the widget renders", () => {
|
|
|
182
154
|
});
|
|
183
155
|
test("the checkbox is labelled once the translations are ready", () => {
|
|
184
156
|
render(props());
|
|
185
|
-
expect(
|
|
157
|
+
expect(checkbox().getAttribute("aria-label")).toBe("WIDGET.I_AM_HUMAN");
|
|
186
158
|
});
|
|
187
159
|
test("the label is left empty while the translations load", () => {
|
|
188
160
|
mocks.translationsReady.current = false;
|
|
189
161
|
render(props());
|
|
190
|
-
expect(
|
|
162
|
+
expect(checkbox().getAttribute("aria-label")).toBe("");
|
|
191
163
|
});
|
|
192
|
-
test("the widget
|
|
193
|
-
render(props());
|
|
164
|
+
test("the widget builds exactly one manager", async () => {
|
|
194
165
|
render(props());
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
error: {
|
|
198
|
-
message: "from the original manager",
|
|
199
|
-
key: "API.UNKNOWN_ERROR",
|
|
200
|
-
},
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
expect(mocks.checkboxProps.current?.error).toBe("from the original manager");
|
|
166
|
+
await click();
|
|
167
|
+
expect(mocks.constructions).toHaveLength(1);
|
|
204
168
|
});
|
|
205
169
|
});
|
|
206
170
|
describe("language", () => {
|
|
@@ -243,17 +207,17 @@ describe("starting from the checkbox", () => {
|
|
|
243
207
|
showRetry: false,
|
|
244
208
|
});
|
|
245
209
|
});
|
|
246
|
-
test("
|
|
210
|
+
test("a synthetic click never reaches the manager", async () => {
|
|
247
211
|
render(props());
|
|
248
212
|
await click({ trusted: false, clientX: 12, clientY: 34 });
|
|
249
|
-
expect(mocks.start).
|
|
213
|
+
expect(mocks.start).not.toHaveBeenCalled();
|
|
250
214
|
});
|
|
251
215
|
test("a tap reports the coordinates of the first touch", async () => {
|
|
252
216
|
render(props());
|
|
253
217
|
await click({ touches: [{ clientX: 7, clientY: 9 }] });
|
|
254
218
|
expect(mocks.start).toHaveBeenCalledWith(7, 9);
|
|
255
219
|
});
|
|
256
|
-
test("a touch event with no touches falls back to the
|
|
220
|
+
test("a touch event with no touches falls back to the pointer", async () => {
|
|
257
221
|
render(props());
|
|
258
222
|
await click({ touches: [] });
|
|
259
223
|
expect(mocks.start).toHaveBeenCalledWith(0, 0);
|
|
@@ -268,9 +232,8 @@ describe("starting from the checkbox", () => {
|
|
|
268
232
|
clickWithoutWaiting();
|
|
269
233
|
clickWithoutWaiting();
|
|
270
234
|
expect(mocks.start).toHaveBeenCalledTimes(1);
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
});
|
|
235
|
+
release(challengeResponse());
|
|
236
|
+
await settle();
|
|
274
237
|
});
|
|
275
238
|
test("the checkbox shows a spinner while the solve is loading", async () => {
|
|
276
239
|
let release = () => {
|
|
@@ -280,55 +243,52 @@ describe("starting from the checkbox", () => {
|
|
|
280
243
|
}));
|
|
281
244
|
render(props());
|
|
282
245
|
clickWithoutWaiting();
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
expect(
|
|
246
|
+
await settle();
|
|
247
|
+
expect(spinner()).not.toBeNull();
|
|
248
|
+
release(challengeResponse());
|
|
249
|
+
await settle();
|
|
250
|
+
expect(spinner()).toBeNull();
|
|
288
251
|
});
|
|
289
252
|
test("a start that yields no challenge leaves the checkbox in place", async () => {
|
|
290
253
|
mocks.start.mockResolvedValue(undefined);
|
|
291
254
|
render(props());
|
|
292
255
|
await click();
|
|
293
256
|
expect(canvas()).toBeNull();
|
|
294
|
-
expect(
|
|
257
|
+
expect(spinner()).toBeNull();
|
|
295
258
|
});
|
|
296
259
|
test("a start that throws returns the user to a usable checkbox", async () => {
|
|
297
260
|
const onError = vi.fn();
|
|
298
261
|
mocks.start.mockRejectedValue(new Error("provider down"));
|
|
299
262
|
render(props({ callbacks: { onError } }));
|
|
300
263
|
await click();
|
|
301
|
-
expect(
|
|
264
|
+
expect(spinner()).toBeNull();
|
|
302
265
|
expect(onError).toHaveBeenCalledWith(expect.any(Error));
|
|
303
266
|
});
|
|
304
267
|
test("a start that throws without an error callback is still recoverable", async () => {
|
|
305
268
|
mocks.start.mockRejectedValue(new Error("provider down"));
|
|
306
269
|
render(props());
|
|
307
270
|
await click();
|
|
308
|
-
expect(
|
|
271
|
+
expect(spinner()).toBeNull();
|
|
309
272
|
});
|
|
310
273
|
});
|
|
311
274
|
describe("autoStart", () => {
|
|
312
275
|
test("fetches a challenge and opens the puzzle without a click", async () => {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
});
|
|
276
|
+
render(props({ autoStart: true, startCoords: { x: 5, y: 6 } }));
|
|
277
|
+
await settle();
|
|
316
278
|
expect(mocks.start).toHaveBeenCalledWith(5, 6);
|
|
317
279
|
expect(canvas()).not.toBeNull();
|
|
318
280
|
});
|
|
319
281
|
test("starts at the origin when no coordinates were handed over", async () => {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
});
|
|
282
|
+
render(props({ autoStart: true }));
|
|
283
|
+
await settle();
|
|
323
284
|
expect(mocks.start).toHaveBeenCalledWith(0, 0);
|
|
324
285
|
});
|
|
325
286
|
test("a failed autoStart leaves the checkbox usable", async () => {
|
|
326
287
|
mocks.start.mockRejectedValue(new Error("nope"));
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
});
|
|
288
|
+
render(props({ autoStart: true }));
|
|
289
|
+
await settle();
|
|
330
290
|
expect(canvas()).toBeNull();
|
|
331
|
-
expect(
|
|
291
|
+
expect(spinner()).toBeNull();
|
|
332
292
|
});
|
|
333
293
|
test("no autoStart means no solve until the user acts", () => {
|
|
334
294
|
render(props());
|
|
@@ -350,7 +310,7 @@ describe("finishing the drag", () => {
|
|
|
350
310
|
await openPuzzle();
|
|
351
311
|
await complete();
|
|
352
312
|
expect(canvas()).toBeNull();
|
|
353
|
-
expect(
|
|
313
|
+
expect(spinner()).toBeNull();
|
|
354
314
|
});
|
|
355
315
|
test("a rejected solution asks for another go on a fresh challenge", async () => {
|
|
356
316
|
mocks.submitSolution.mockResolvedValue(false);
|
|
@@ -411,22 +371,18 @@ describe("finishing the drag", () => {
|
|
|
411
371
|
release = resolve;
|
|
412
372
|
}));
|
|
413
373
|
await openPuzzle();
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
});
|
|
374
|
+
void mocks.canvasProps.current?.onComplete(200, 80, []);
|
|
375
|
+
await settle();
|
|
417
376
|
expect(mocks.canvasProps.current?.submitting).toBe(true);
|
|
418
|
-
expect(
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
});
|
|
377
|
+
expect(spinner()).not.toBeNull();
|
|
378
|
+
release(true);
|
|
379
|
+
await settle();
|
|
422
380
|
});
|
|
423
381
|
});
|
|
424
382
|
describe("invisible mode", () => {
|
|
425
383
|
const execute = async () => {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
await Promise.resolve();
|
|
429
|
-
});
|
|
384
|
+
document.dispatchEvent(new Event("procaptcha:execute"));
|
|
385
|
+
await settle();
|
|
430
386
|
};
|
|
431
387
|
test("an execute event fetches a challenge and opens the puzzle", async () => {
|
|
432
388
|
render(props({ config: config({ mode: ModeEnum.invisible }) }));
|
|
@@ -442,10 +398,8 @@ describe("invisible mode", () => {
|
|
|
442
398
|
test("a targeted execute on the container runs a visible widget", async () => {
|
|
443
399
|
const target = document.createElement("div");
|
|
444
400
|
render(props({ container: target }));
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
await Promise.resolve();
|
|
448
|
-
});
|
|
401
|
+
target.dispatchEvent(new Event("procaptcha:execute"));
|
|
402
|
+
await settle();
|
|
449
403
|
expect(mocks.start).toHaveBeenCalledTimes(1);
|
|
450
404
|
expect(canvas()).not.toBeNull();
|
|
451
405
|
});
|
|
@@ -455,10 +409,8 @@ describe("invisible mode", () => {
|
|
|
455
409
|
config: config({ mode: ModeEnum.invisible }),
|
|
456
410
|
container: target,
|
|
457
411
|
}));
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
await Promise.resolve();
|
|
461
|
-
});
|
|
412
|
+
target.dispatchEvent(new Event("procaptcha:execute"));
|
|
413
|
+
await settle();
|
|
462
414
|
expect(mocks.start).toHaveBeenCalledTimes(1);
|
|
463
415
|
});
|
|
464
416
|
test("an execute that yields no challenge shows no puzzle", async () => {
|
|
@@ -487,85 +439,66 @@ describe("invisible mode", () => {
|
|
|
487
439
|
await execute();
|
|
488
440
|
expect(onError).toHaveBeenCalledWith(expect.any(Error));
|
|
489
441
|
});
|
|
490
|
-
test("the listener is dropped when the widget
|
|
442
|
+
test("the listener is dropped when the widget is destroyed", async () => {
|
|
491
443
|
render(props({ config: config({ mode: ModeEnum.invisible }) }));
|
|
492
|
-
|
|
493
|
-
root.unmount();
|
|
494
|
-
});
|
|
495
|
-
act(() => {
|
|
496
|
-
root = createRoot(container);
|
|
497
|
-
});
|
|
444
|
+
destroy();
|
|
498
445
|
await execute();
|
|
499
446
|
expect(mocks.start).not.toHaveBeenCalled();
|
|
500
447
|
});
|
|
501
448
|
});
|
|
502
449
|
describe("an invalidated session", () => {
|
|
503
|
-
const invalidate = (key = "CAPTCHA.NO_SESSION_FOUND", message = "session gone") => {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
});
|
|
450
|
+
const invalidate = async (key = "CAPTCHA.NO_SESSION_FOUND", message = "session gone") => {
|
|
451
|
+
mocks.constructions[0]?.updateState({ error: { message, key } });
|
|
452
|
+
await settle();
|
|
507
453
|
};
|
|
508
454
|
test("the error is shown on the checkbox and the puzzle is torn down", async () => {
|
|
509
455
|
render(props());
|
|
510
456
|
await click();
|
|
511
|
-
invalidate("API.UNKNOWN_ERROR", "something broke");
|
|
512
|
-
expect(
|
|
457
|
+
await invalidate("API.UNKNOWN_ERROR", "something broke");
|
|
458
|
+
expect(mounted.container.textContent).toContain("something broke");
|
|
513
459
|
expect(canvas()).toBeNull();
|
|
514
|
-
expect(
|
|
460
|
+
expect(spinner()).toBeNull();
|
|
515
461
|
});
|
|
516
462
|
test("the host is told to re-mint, with the coordinates of the original click", async () => {
|
|
517
463
|
const onSessionInvalidated = vi.fn();
|
|
518
464
|
render(props({ onSessionInvalidated }));
|
|
519
465
|
await click({ clientX: 12, clientY: 34 });
|
|
520
|
-
invalidate();
|
|
466
|
+
await invalidate();
|
|
521
467
|
expect(onSessionInvalidated).toHaveBeenCalledWith(12, 34);
|
|
522
468
|
});
|
|
523
469
|
test("the host is told only once, however many errors arrive", async () => {
|
|
524
470
|
const onSessionInvalidated = vi.fn();
|
|
525
471
|
render(props({ onSessionInvalidated }));
|
|
526
472
|
await click();
|
|
527
|
-
invalidate("CAPTCHA.NO_SESSION_FOUND", "gone");
|
|
528
|
-
invalidate("CAPTCHA.NO_SESSION_FOUND", "gone again");
|
|
473
|
+
await invalidate("CAPTCHA.NO_SESSION_FOUND", "gone");
|
|
474
|
+
await invalidate("CAPTCHA.NO_SESSION_FOUND", "gone again");
|
|
529
475
|
expect(onSessionInvalidated).toHaveBeenCalledTimes(1);
|
|
530
476
|
});
|
|
531
477
|
test("with no host handler the frictionless session restarts instead", async () => {
|
|
532
|
-
vi.useFakeTimers();
|
|
533
478
|
const restart = vi.fn();
|
|
534
479
|
render(props({ frictionlessState: frictionless({ restart }) }));
|
|
535
|
-
invalidate();
|
|
480
|
+
await invalidate();
|
|
536
481
|
expect(restart).not.toHaveBeenCalled();
|
|
537
|
-
await
|
|
538
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
539
|
-
});
|
|
482
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
540
483
|
expect(restart).toHaveBeenCalledTimes(1);
|
|
541
|
-
vi.useRealTimers();
|
|
542
484
|
});
|
|
543
|
-
test("a restart pending at
|
|
544
|
-
vi.useFakeTimers();
|
|
485
|
+
test("a restart pending at destroy is cancelled", async () => {
|
|
545
486
|
const restart = vi.fn();
|
|
546
487
|
render(props({ frictionlessState: frictionless({ restart }) }));
|
|
547
|
-
invalidate();
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
});
|
|
551
|
-
await act(async () => {
|
|
552
|
-
await vi.advanceTimersByTimeAsync(200);
|
|
553
|
-
});
|
|
488
|
+
await invalidate();
|
|
489
|
+
destroy();
|
|
490
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
554
491
|
expect(restart).not.toHaveBeenCalled();
|
|
555
|
-
act(() => {
|
|
556
|
-
root = createRoot(container);
|
|
557
|
-
});
|
|
558
|
-
vi.useRealTimers();
|
|
559
492
|
});
|
|
560
493
|
test("a lost session with nothing to recover it is simply surfaced", async () => {
|
|
561
494
|
render(props());
|
|
562
|
-
invalidate();
|
|
563
|
-
expect(
|
|
495
|
+
await invalidate();
|
|
496
|
+
expect(mounted.container.textContent).toContain("session gone");
|
|
564
497
|
});
|
|
565
|
-
test("no coordinates are reported when the session was never clicked into", () => {
|
|
498
|
+
test("no coordinates are reported when the session was never clicked into", async () => {
|
|
566
499
|
const onSessionInvalidated = vi.fn();
|
|
567
500
|
render(props({ onSessionInvalidated }));
|
|
568
|
-
invalidate();
|
|
501
|
+
await invalidate();
|
|
569
502
|
expect(onSessionInvalidated).toHaveBeenCalledWith(undefined, undefined);
|
|
570
503
|
});
|
|
571
504
|
});
|