@prosopo/procaptcha-pow 2.12.6 → 2.13.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.
Files changed (50) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +5 -5
  2. package/.turbo/turbo-build$colon$tsc.log +19 -19
  3. package/.turbo/turbo-build.log +6 -6
  4. package/CHANGELOG.md +53 -0
  5. package/dist/cjs/components/procaptchaPoW.cjs +22 -0
  6. package/dist/cjs/components/procaptchaWidget.cjs +143 -0
  7. package/dist/cjs/index.cjs +5 -2
  8. package/dist/components/procaptchaPoW.d.ts +7 -0
  9. package/dist/components/procaptchaPoW.d.ts.map +1 -0
  10. package/dist/components/procaptchaPoW.js +21 -0
  11. package/dist/components/procaptchaPoW.js.map +1 -0
  12. package/dist/components/procaptchaWidget.d.ts +7 -0
  13. package/dist/components/procaptchaWidget.d.ts.map +1 -0
  14. package/dist/components/procaptchaWidget.js +143 -0
  15. package/dist/components/procaptchaWidget.js.map +1 -0
  16. package/dist/index.d.ts +2 -2
  17. package/dist/index.js +3 -2
  18. package/dist/tests/domHarness.d.ts +20 -0
  19. package/dist/tests/domHarness.d.ts.map +1 -0
  20. package/dist/tests/domHarness.js +56 -0
  21. package/dist/tests/domHarness.js.map +1 -0
  22. package/dist/tests/procaptchaPoW.unit.test.js +46 -30
  23. package/dist/tests/procaptchaPoW.unit.test.js.map +1 -1
  24. package/dist/tests/procaptchaPow.test-d.js +5 -5
  25. package/dist/tests/procaptchaPow.test-d.js.map +1 -1
  26. package/dist/tests/procaptchaWidget.unit.test.js +79 -137
  27. package/dist/tests/procaptchaWidget.unit.test.js.map +1 -1
  28. package/package.json +9 -13
  29. package/src/components/procaptchaPoW.ts +53 -0
  30. package/src/components/procaptchaWidget.ts +281 -0
  31. package/src/index.ts +2 -2
  32. package/src/tests/domHarness.ts +122 -0
  33. package/src/tests/procaptchaPoW.unit.test.ts +59 -36
  34. package/src/tests/procaptchaPow.test-d.ts +15 -9
  35. package/src/tests/procaptchaWidget.unit.test.ts +105 -171
  36. package/tsconfig.cjs.json +1 -2
  37. package/tsconfig.json +1 -2
  38. package/tsconfig.tsbuildinfo +1 -1
  39. package/dist/cjs/components/ProcaptchaPoW.cjs +0 -7
  40. package/dist/cjs/components/ProcaptchaWidget.cjs +0 -130
  41. package/dist/components/ProcaptchaPoW.d.ts +0 -3
  42. package/dist/components/ProcaptchaPoW.d.ts.map +0 -1
  43. package/dist/components/ProcaptchaPoW.js +0 -7
  44. package/dist/components/ProcaptchaPoW.js.map +0 -1
  45. package/dist/components/ProcaptchaWidget.d.ts +0 -4
  46. package/dist/components/ProcaptchaWidget.d.ts.map +0 -1
  47. package/dist/components/ProcaptchaWidget.js +0 -131
  48. package/dist/components/ProcaptchaWidget.js.map +0 -1
  49. package/src/components/ProcaptchaPoW.tsx +0 -39
  50. package/src/components/ProcaptchaWidget.tsx +0 -231
@@ -18,8 +18,6 @@ import {
18
18
  type ProcaptchaProps,
19
19
  type ProcaptchaState,
20
20
  } from "@prosopo/types";
21
- import { type ReactElement, act, createElement } from "react";
22
- import { type Root, createRoot } from "react-dom/client";
23
21
  import {
24
22
  type Mock,
25
23
  afterEach,
@@ -29,7 +27,11 @@ import {
29
27
  test,
30
28
  vi,
31
29
  } from "vitest";
32
- import Procaptcha from "../components/ProcaptchaWidget.js";
30
+ import {
31
+ type ProcaptchaPowHandle,
32
+ mountProcaptchaPowWidget,
33
+ } from "../components/procaptchaWidget.js";
34
+ import { type Mounted, fire, mount, settle } from "./domHarness.js";
33
35
  import { config, frictionless } from "./managerHarness.js";
34
36
 
35
37
  /**
@@ -45,18 +47,6 @@ const mocks = vi.hoisted(() => {
45
47
  getHoneypotValue?: () => string | undefined;
46
48
  }[] = [];
47
49
  const loadI18next = vi.fn<(a?: boolean, b?: string) => Promise<unknown>>();
48
- const checkboxProps: {
49
- current:
50
- | {
51
- checked: boolean;
52
- loading: boolean;
53
- labelText: string;
54
- error?: string;
55
- onChange: (event: { nativeEvent: unknown }) => Promise<void>;
56
- }
57
- | undefined;
58
- } = { current: undefined };
59
- const honeypotQuestions: string[] = [];
60
50
  const translationsReady = { current: true };
61
51
  return {
62
52
  translationsReady,
@@ -64,8 +54,6 @@ const mocks = vi.hoisted(() => {
64
54
  resetState,
65
55
  constructions,
66
56
  loadI18next,
67
- checkboxProps,
68
- honeypotQuestions,
69
57
  };
70
58
  });
71
59
 
@@ -84,56 +72,22 @@ vi.mock("../services/Manager.js", () => ({
84
72
  },
85
73
  }));
86
74
 
87
- // The checkbox and the honeypot are procaptcha-common's, and tested there. The
88
- // stubs keep their contract — a change callback and a forwarded input ref —
89
- // while letting a test drive the exact browser event the widget branches on,
90
- // which jsdom cannot produce (it marks every dispatched event untrusted, and
91
- // the real checkbox drops those before the widget ever sees them).
92
- vi.mock("@prosopo/procaptcha-common", async (importOriginal) => {
93
- const actual =
94
- await importOriginal<typeof import("@prosopo/procaptcha-common")>();
95
- const { createElement, forwardRef } = await import("react");
96
- interface CheckboxStubProps {
97
- checked: boolean;
98
- loading: boolean;
99
- labelText: string;
100
- error?: string;
101
- onChange: (event: { nativeEvent: unknown }) => Promise<void>;
102
- }
103
- const Checkbox = (props: CheckboxStubProps) => {
104
- mocks.checkboxProps.current = props;
105
- return createElement("input", {
106
- type: "checkbox",
107
- readOnly: true,
108
- checked: props.checked,
109
- "aria-label": props.labelText,
110
- "data-error": props.error,
111
- "data-loading": String(props.loading),
112
- });
113
- };
114
- const Honeypot = forwardRef<HTMLInputElement, { encodedQuestion: string }>(
115
- ({ encodedQuestion }, ref) => {
116
- mocks.honeypotQuestions.push(encodedQuestion);
117
- return createElement("input", { type: "text", ref, name: "honeypot" });
118
- },
119
- );
120
- return { ...actual, Checkbox, Honeypot };
121
- });
122
-
123
75
  vi.mock("@prosopo/locale", async (importOriginal) => {
124
76
  const actual = await importOriginal<typeof import("@prosopo/locale")>();
125
77
  return {
126
78
  ...actual,
127
79
  loadI18next: mocks.loadI18next,
128
- useTranslation: () => ({
80
+ createTranslator: () => ({
129
81
  t: (key: string) => key,
130
- ready: mocks.translationsReady.current,
82
+ isReady: () => mocks.translationsReady.current,
83
+ subscribe: () => () => undefined,
84
+ i18n: undefined,
131
85
  }),
132
86
  };
133
87
  });
134
88
 
135
- let container: HTMLDivElement;
136
- let root: Root;
89
+ let mounted: Mounted;
90
+ let widget: ProcaptchaPowHandle | undefined;
137
91
 
138
92
  const i18nStub = (
139
93
  language: string,
@@ -148,36 +102,28 @@ const props = (overrides: Partial<ProcaptchaProps> = {}): ProcaptchaProps => ({
148
102
  });
149
103
 
150
104
  const render = (widgetProps: ProcaptchaProps): void => {
151
- act(() => {
152
- root.render(createElement(Procaptcha, widgetProps) as ReactElement);
153
- });
105
+ widget = mountProcaptchaPowWidget(mounted.container, widgetProps);
154
106
  };
155
107
 
156
108
  beforeEach(() => {
157
109
  vi.clearAllMocks();
158
110
  mocks.constructions.length = 0;
159
- mocks.honeypotQuestions.length = 0;
160
111
  mocks.translationsReady.current = true;
161
- mocks.checkboxProps.current = undefined;
162
112
  mocks.start.mockResolvedValue(undefined);
163
113
  mocks.loadI18next.mockResolvedValue(undefined);
164
- container = document.createElement("div");
165
- document.body.appendChild(container);
166
- act(() => {
167
- root = createRoot(container);
168
- });
114
+ widget = undefined;
115
+ mounted = mount();
169
116
  });
170
117
 
171
118
  afterEach(() => {
172
- act(() => {
173
- root.unmount();
174
- });
175
- container.remove();
119
+ widget?.destroy();
120
+ widget = undefined;
121
+ mounted.unmount();
176
122
  vi.restoreAllMocks();
177
123
  });
178
124
 
179
125
  const checkbox = (): HTMLInputElement => {
180
- const element = container.querySelector<HTMLInputElement>(
126
+ const element = mounted.container.querySelector<HTMLInputElement>(
181
127
  'input[type="checkbox"]',
182
128
  );
183
129
  if (!element) throw new Error("expected a checkbox to be rendered");
@@ -185,13 +131,16 @@ const checkbox = (): HTMLInputElement => {
185
131
  };
186
132
 
187
133
  const honeypotInput = (): HTMLInputElement => {
188
- const element = container.querySelector<HTMLInputElement>(
189
- 'input[name="honeypot"]',
134
+ const element = document.querySelector<HTMLInputElement>(
135
+ 'input[name="email_confirm"]',
190
136
  );
191
137
  if (!element) throw new Error("expected a honeypot to be rendered");
192
138
  return element;
193
139
  };
194
140
 
141
+ const spinner = (): Element | null =>
142
+ mounted.container.querySelector('[role="status"]');
143
+
195
144
  interface ClickOptions {
196
145
  trusted?: boolean;
197
146
  clientX?: number;
@@ -199,17 +148,14 @@ interface ClickOptions {
199
148
  touches?: { clientX: number; clientY: number }[];
200
149
  }
201
150
 
202
- /** Hand the widget the browser event a click on the checkbox would produce. */
151
+ /** Click the real checkbox with the browser event a user would produce. */
203
152
  const click = (options: ClickOptions = {}): void => {
204
- const nativeEvent = {
205
- isTrusted: options.trusted ?? true,
206
- clientX: options.clientX ?? 0,
207
- clientY: options.clientY ?? 0,
208
- ...(options.touches ? { touches: options.touches } : {}),
209
- };
210
- act(() => {
211
- void mocks.checkboxProps.current?.onChange({ nativeEvent });
212
- });
153
+ fire(checkbox(), "click", options);
154
+ };
155
+
156
+ const setState = async (next: Partial<ProcaptchaState>): Promise<void> => {
157
+ mocks.constructions[0]?.updateState(next);
158
+ await settle();
213
159
  };
214
160
 
215
161
  describe("what the widget renders", () => {
@@ -220,32 +166,38 @@ describe("what the widget renders", () => {
220
166
 
221
167
  test("an invisible widget shows no checkbox at all", () => {
222
168
  render(props({ config: config({ mode: ModeEnum.invisible }) }));
223
- expect(container.querySelector('[aria-label="human checkbox"]')).toBeNull();
169
+ expect(
170
+ mounted.container.querySelector('input[type="checkbox"]'),
171
+ ).toBeNull();
224
172
  });
225
173
 
226
174
  test("a session with a honeypot question renders the honeypot", () => {
227
- render(props({ frictionlessState: frictionless({ hp: "question" }) }));
175
+ render(
176
+ props({ frictionlessState: frictionless({ hp: btoa("question") }) }),
177
+ );
228
178
  expect(honeypotInput()).toBeDefined();
229
- expect(mocks.honeypotQuestions.at(-1)).toBe("question");
179
+ expect(document.body.textContent).toContain("question");
230
180
  });
231
181
 
232
182
  test("a session without one renders no bait at all", () => {
233
183
  render(props({ frictionlessState: frictionless() }));
234
- expect(container.querySelector('input[name="honeypot"]')).toBeNull();
184
+ expect(document.querySelector('input[name="email_confirm"]')).toBeNull();
235
185
  });
236
186
 
237
187
  test("an invisible widget still renders the honeypot, as bait", () => {
238
188
  render(
239
189
  props({
240
190
  config: config({ mode: ModeEnum.invisible }),
241
- frictionlessState: frictionless({ hp: "question" }),
191
+ frictionlessState: frictionless({ hp: btoa("question") }),
242
192
  }),
243
193
  );
244
194
  expect(honeypotInput()).toBeDefined();
245
195
  });
246
196
 
247
197
  test("the manager can read the honeypot input the widget rendered", () => {
248
- render(props({ frictionlessState: frictionless({ hp: "question" }) }));
198
+ render(
199
+ props({ frictionlessState: frictionless({ hp: btoa("question") }) }),
200
+ );
249
201
  honeypotInput().value = "bot@example.com";
250
202
  expect(mocks.constructions[0]?.getHoneypotValue?.()).toBe(
251
203
  "bot@example.com",
@@ -259,14 +211,14 @@ describe("what the widget renders", () => {
259
211
 
260
212
  test("the checkbox is labelled once the translations are ready", () => {
261
213
  render(props());
262
- expect(mocks.checkboxProps.current?.labelText).toBe("WIDGET.I_AM_HUMAN");
214
+ expect(checkbox().getAttribute("aria-label")).toBe("WIDGET.I_AM_HUMAN");
263
215
  });
264
216
 
265
217
  test("the checkbox is left unlabelled until the translations load", () => {
266
218
  // A key like WIDGET.I_AM_HUMAN rendered raw is worse than no label.
267
219
  mocks.translationsReady.current = false;
268
220
  render(props());
269
- expect(mocks.checkboxProps.current?.labelText).toBe("");
221
+ expect(checkbox().getAttribute("aria-label")).toBe("");
270
222
  });
271
223
 
272
224
  test("a page that registered no callbacks still renders", () => {
@@ -280,18 +232,18 @@ describe("what the widget renders", () => {
280
232
  expect(checkbox()).toBeDefined();
281
233
  });
282
234
 
283
- test("the checkbox shows the current error", () => {
235
+ test("the checkbox shows the current error", async () => {
284
236
  render(props());
285
- act(() => {
286
- mocks.constructions[0]?.updateState({
287
- error: { message: "no session", key: "API.UNKNOWN_ERROR" },
288
- });
237
+ await setState({
238
+ error: { message: "no session", key: "API.UNKNOWN_ERROR" },
289
239
  });
290
- expect(mocks.checkboxProps.current?.error).toBe("no session");
240
+ expect(mounted.container.textContent).toContain("no session");
291
241
  });
292
242
 
293
243
  test("an unfilled honeypot reads as nothing, not as an empty answer", () => {
294
- render(props({ frictionlessState: frictionless({ hp: "question" }) }));
244
+ render(
245
+ props({ frictionlessState: frictionless({ hp: btoa("question") }) }),
246
+ );
295
247
  expect(mocks.constructions[0]?.getHoneypotValue?.()).toBeUndefined();
296
248
  });
297
249
 
@@ -299,6 +251,15 @@ describe("what the widget renders", () => {
299
251
  render(props());
300
252
  expect(mocks.constructions[0]?.getHoneypotValue?.()).toBeUndefined();
301
253
  });
254
+
255
+ test("the honeypot leaves the page with the widget", () => {
256
+ render(
257
+ props({ frictionlessState: frictionless({ hp: btoa("question") }) }),
258
+ );
259
+ widget?.destroy();
260
+ widget = undefined;
261
+ expect(document.querySelector('input[name="email_confirm"]')).toBeNull();
262
+ });
302
263
  });
303
264
 
304
265
  describe("starting a solve", () => {
@@ -320,21 +281,18 @@ describe("starting a solve", () => {
320
281
  expect(mocks.start).toHaveBeenCalledWith(3, 4);
321
282
  });
322
283
 
323
- test("an untrusted click solves, but its coordinates are discarded", () => {
324
- // A synthetic click is what an automated solver produces; recording its
325
- // coordinates would launder them into the salt as real telemetry.
284
+ test("a synthetic click never reaches the manager", () => {
285
+ // The checkbox drops untrusted events before the widget sees them, so an
286
+ // automated solver cannot start a session at all. The widget's own
287
+ // isTrusted guard on the coordinates is defence in depth behind it.
326
288
  render(props());
327
289
  click({ trusted: false, clientX: 12, clientY: 34 });
328
- expect(mocks.start).toHaveBeenCalledWith(0, 0);
290
+ expect(mocks.start).not.toHaveBeenCalled();
329
291
  });
330
292
 
331
- test("an event that reports no position at all is treated as the origin", () => {
293
+ test("a keyboard activation is treated as the origin", () => {
332
294
  render(props());
333
- act(() => {
334
- void mocks.checkboxProps.current?.onChange({
335
- nativeEvent: { isTrusted: true },
336
- });
337
- });
295
+ fire(checkbox(), "keydown", { key: "Enter" });
338
296
  expect(mocks.start).toHaveBeenCalledWith(0, 0);
339
297
  });
340
298
 
@@ -342,46 +300,43 @@ describe("starting a solve", () => {
342
300
  let release = (): void => undefined;
343
301
  mocks.start.mockImplementation(
344
302
  () =>
345
- new Promise<void>((resolve) => {
303
+ new Promise<void>((resolve: () => void) => {
346
304
  release = () => resolve();
347
305
  }),
348
306
  );
349
307
  render(props());
350
308
  click();
351
- expect(mocks.checkboxProps.current?.loading).toBe(true);
352
- await act(async () => {
353
- release();
354
- });
355
- expect(mocks.checkboxProps.current?.loading).toBe(false);
309
+ await settle();
310
+ expect(spinner()).not.toBeNull();
311
+ release();
312
+ await settle();
313
+ expect(spinner()).toBeNull();
356
314
  });
357
315
 
358
316
  test("a solve that rejects still clears the spinner", async () => {
359
- // Nothing else would: React ignores the promise, so the rejection used to
360
- // escape as an unhandled rejection with the spinner still turning.
317
+ // Nothing else would: the promise is not awaited by the caller, so the
318
+ // rejection used to escape unhandled with the spinner still turning.
361
319
  vi.spyOn(console, "error").mockImplementation(() => undefined);
362
320
  mocks.start.mockRejectedValue(new Error("provider down"));
363
321
  render(props({ autoStart: true }));
364
- await act(async () => undefined);
365
- expect(mocks.checkboxProps.current?.loading).toBe(false);
322
+ await settle();
323
+ expect(spinner()).toBeNull();
366
324
  });
367
325
 
368
326
  test("a click whose solve rejects clears the spinner too", async () => {
369
327
  vi.spyOn(console, "error").mockImplementation(() => undefined);
370
328
  mocks.start.mockRejectedValue(new Error("provider down"));
371
329
  render(props());
372
- await act(async () => {
373
- await mocks.checkboxProps.current?.onChange({
374
- nativeEvent: { isTrusted: true, clientX: 1, clientY: 2 },
375
- });
376
- });
377
- expect(mocks.checkboxProps.current?.loading).toBe(false);
330
+ click({ clientX: 1, clientY: 2 });
331
+ await settle();
332
+ expect(spinner()).toBeNull();
378
333
  });
379
334
 
380
335
  test("a second click while the first solve is running is ignored", async () => {
381
336
  let release = (): void => undefined;
382
337
  mocks.start.mockImplementation(
383
338
  () =>
384
- new Promise<void>((resolve) => {
339
+ new Promise<void>((resolve: () => void) => {
385
340
  release = () => resolve();
386
341
  }),
387
342
  );
@@ -389,9 +344,8 @@ describe("starting a solve", () => {
389
344
  click();
390
345
  click();
391
346
  expect(mocks.start).toHaveBeenCalledTimes(1);
392
- await act(async () => {
393
- release();
394
- });
347
+ release();
348
+ await settle();
395
349
  });
396
350
 
397
351
  test("an autoStart widget solves without waiting for a click", () => {
@@ -412,9 +366,7 @@ describe("starting a solve", () => {
412
366
 
413
367
  describe("the invisible-mode execute event", () => {
414
368
  const execute = (): void => {
415
- act(() => {
416
- document.dispatchEvent(new Event("procaptcha:execute"));
417
- });
369
+ document.dispatchEvent(new Event("procaptcha:execute"));
418
370
  };
419
371
 
420
372
  test("starts a solve", () => {
@@ -430,10 +382,8 @@ describe("the invisible-mode execute event", () => {
430
382
  });
431
383
 
432
384
  const executeOn = async (target: Element): Promise<void> => {
433
- await act(async () => {
434
- target.dispatchEvent(new Event("procaptcha:execute"));
435
- await new Promise((resolve) => setTimeout(resolve, 0));
436
- });
385
+ target.dispatchEvent(new Event("procaptcha:execute"));
386
+ await settle();
437
387
  };
438
388
 
439
389
  test("a targeted execute on the container runs a visible widget", async () => {
@@ -457,10 +407,8 @@ describe("the invisible-mode execute event", () => {
457
407
 
458
408
  test("stops being listened for once the widget is gone", () => {
459
409
  render(props({ config: config({ mode: ModeEnum.invisible }) }));
460
- act(() => {
461
- root.unmount();
462
- root = createRoot(container);
463
- });
410
+ widget?.destroy();
411
+ widget = undefined;
464
412
  execute();
465
413
  expect(mocks.start).not.toHaveBeenCalled();
466
414
  });
@@ -481,65 +429,51 @@ describe("the invisible-mode execute event", () => {
481
429
  mocks.start.mockRejectedValue(new Error("no provider"));
482
430
  render(props({ config: config({ mode: ModeEnum.invisible }) }));
483
431
  execute();
484
- await act(async () => undefined);
432
+ await settle();
485
433
  expect(reported).toHaveBeenCalled();
486
434
  });
487
435
  });
488
436
 
489
437
  describe("an invalidated session", () => {
490
- const fail = (key: string): void => {
491
- act(() => {
492
- mocks.constructions[0]?.updateState({
493
- error: { message: "boom", key },
494
- });
495
- });
438
+ const fail = async (key: string): Promise<void> => {
439
+ await setState({ error: { message: "boom", key } });
496
440
  };
497
441
 
498
- test("is handed to the recovery-aware parent, with the original click", () => {
442
+ test("is handed to the recovery-aware parent, with the original click", async () => {
499
443
  const onSessionInvalidated = vi.fn<(x?: number, y?: number) => void>();
500
444
  render(props({ onSessionInvalidated }));
501
445
  click({ clientX: 9, clientY: 8 });
502
- fail("CAPTCHA.NO_SESSION_FOUND");
446
+ await fail("CAPTCHA.NO_SESSION_FOUND");
503
447
  expect(onSessionInvalidated).toHaveBeenCalledWith(9, 8);
504
448
  });
505
449
 
506
- test("is escalated once only, so a failing retry cannot loop", () => {
450
+ test("is escalated once only, so a failing retry cannot loop", async () => {
507
451
  const onSessionInvalidated = vi.fn<(x?: number, y?: number) => void>();
508
452
  const restart = vi.fn<() => void>();
509
- vi.useFakeTimers();
510
453
  render(
511
454
  props({
512
455
  onSessionInvalidated,
513
456
  frictionlessState: frictionless({ restart }),
514
457
  }),
515
458
  );
516
- fail("CAPTCHA.NO_SESSION_FOUND");
517
- act(() => {
518
- mocks.constructions[0]?.updateState({ error: undefined });
519
- });
520
- fail("CAPTCHA.NO_SESSION_FOUND");
459
+ await fail("CAPTCHA.NO_SESSION_FOUND");
460
+ await setState({ error: undefined });
461
+ await fail("CAPTCHA.NO_SESSION_FOUND");
521
462
  expect(onSessionInvalidated).toHaveBeenCalledTimes(1);
522
- act(() => {
523
- vi.advanceTimersByTime(100);
524
- });
463
+ await new Promise<void>((resolve: () => void) => setTimeout(resolve, 150));
525
464
  expect(restart).toHaveBeenCalledTimes(1);
526
- vi.useRealTimers();
527
465
  });
528
466
 
529
- test("without a recovery-aware parent the frictionless session restarts", () => {
467
+ test("without a recovery-aware parent the frictionless session restarts", async () => {
530
468
  const restart = vi.fn<() => void>();
531
- vi.useFakeTimers();
532
469
  render(props({ frictionlessState: frictionless({ restart }) }));
533
- fail("CAPTCHA.NO_SESSION_FOUND");
470
+ await fail("CAPTCHA.NO_SESSION_FOUND");
534
471
  expect(restart).not.toHaveBeenCalled();
535
- act(() => {
536
- vi.advanceTimersByTime(100);
537
- });
472
+ await new Promise<void>((resolve: () => void) => setTimeout(resolve, 150));
538
473
  expect(restart).toHaveBeenCalledTimes(1);
539
- vi.useRealTimers();
540
474
  });
541
475
 
542
- test("any other error is left for the widget to display", () => {
476
+ test("any other error is left for the widget to display", async () => {
543
477
  const onSessionInvalidated = vi.fn<(x?: number, y?: number) => void>();
544
478
  const restart = vi.fn<() => void>();
545
479
  render(
@@ -548,14 +482,14 @@ describe("an invalidated session", () => {
548
482
  frictionlessState: frictionless({ restart }),
549
483
  }),
550
484
  );
551
- fail("API.UNKNOWN_ERROR");
485
+ await fail("API.UNKNOWN_ERROR");
552
486
  expect(onSessionInvalidated).not.toHaveBeenCalled();
553
487
  expect(restart).not.toHaveBeenCalled();
554
488
  });
555
489
 
556
- test("an error with nowhere to escalate to is simply shown", () => {
490
+ test("an error with nowhere to escalate to is simply shown", async () => {
557
491
  render(props());
558
- expect(() => fail("CAPTCHA.NO_SESSION_FOUND")).not.toThrow();
492
+ await expect(fail("CAPTCHA.NO_SESSION_FOUND")).resolves.toBeUndefined();
559
493
  });
560
494
  });
561
495
 
package/tsconfig.cjs.json CHANGED
@@ -3,8 +3,7 @@
3
3
  "compilerOptions": {
4
4
  "rootDir": "./src",
5
5
  "outDir": "./dist/cjs",
6
- "lib": ["es6", "dom"],
7
- "jsxImportSource": "@emotion/react"
6
+ "lib": ["es6", "dom"]
8
7
  },
9
8
  "include": [
10
9
  "./src/**/*.ts",
package/tsconfig.json CHANGED
@@ -3,8 +3,7 @@
3
3
  "compilerOptions": {
4
4
  "rootDir": "./src",
5
5
  "outDir": "./dist",
6
- "lib": ["es6", "dom"],
7
- "jsxImportSource": "@emotion/react"
6
+ "lib": ["es6", "dom"]
8
7
  },
9
8
  "include": [
10
9
  "src",