@prosopo/procaptcha-puzzle 2.14.6 → 2.15.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 (47) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +5 -8
  2. package/.turbo/turbo-build$colon$tsc.log +19 -22
  3. package/.turbo/turbo-build.log +6 -12
  4. package/CHANGELOG.md +68 -0
  5. package/LICENSE +201 -0
  6. package/dist/cjs/components/procaptchaWidget.cjs +20 -5
  7. package/dist/cjs/components/puzzleCanvas.cjs +25 -4
  8. package/dist/cjs/services/Manager.cjs +18 -2
  9. package/dist/components/procaptchaWidget.d.ts.map +1 -1
  10. package/dist/components/procaptchaWidget.js +21 -6
  11. package/dist/components/procaptchaWidget.js.map +1 -1
  12. package/dist/components/puzzleCanvas.d.ts +2 -1
  13. package/dist/components/puzzleCanvas.d.ts.map +1 -1
  14. package/dist/components/puzzleCanvas.js +26 -5
  15. package/dist/components/puzzleCanvas.js.map +1 -1
  16. package/dist/services/Manager.d.ts +1 -0
  17. package/dist/services/Manager.d.ts.map +1 -1
  18. package/dist/services/Manager.js +18 -2
  19. package/dist/services/Manager.js.map +1 -1
  20. package/dist/tests/invisibleCanvasVisibility.unit.test.js +3 -1
  21. package/dist/tests/invisibleCanvasVisibility.unit.test.js.map +1 -1
  22. package/dist/tests/manager.unit.test.js +16 -0
  23. package/dist/tests/manager.unit.test.js.map +1 -1
  24. package/dist/tests/procaptchaPuzzle.test-d.js +1 -1
  25. package/dist/tests/procaptchaPuzzle.test-d.js.map +1 -1
  26. package/dist/tests/procaptchaWidget.unit.test.js +73 -0
  27. package/dist/tests/procaptchaWidget.unit.test.js.map +1 -1
  28. package/dist/tests/puzzleCanvas.unit.test.js +33 -0
  29. package/dist/tests/puzzleCanvas.unit.test.js.map +1 -1
  30. package/package.json +72 -71
  31. package/src/components/procaptchaPuzzle.ts +0 -54
  32. package/src/components/procaptchaWidget.ts +0 -422
  33. package/src/components/puzzleCanvas.ts +0 -658
  34. package/src/index.ts +0 -16
  35. package/src/services/Manager.ts +0 -508
  36. package/src/tests/domHarness.ts +0 -122
  37. package/src/tests/invisibleCanvasVisibility.unit.test.ts +0 -179
  38. package/src/tests/manager.unit.test.ts +0 -822
  39. package/src/tests/managerHarness.ts +0 -169
  40. package/src/tests/procaptchaPuzzle.test-d.ts +0 -212
  41. package/src/tests/procaptchaPuzzle.unit.test.ts +0 -138
  42. package/src/tests/procaptchaWidget.unit.test.ts +0 -765
  43. package/src/tests/puzzleCanvas.unit.test.ts +0 -662
  44. package/tsconfig.cjs.json +0 -43
  45. package/tsconfig.json +0 -44
  46. package/tsconfig.tsbuildinfo +0 -1
  47. package/tsconfig.types.json +0 -9
@@ -1,662 +0,0 @@
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 { Translator } from "@prosopo/locale";
16
- import type { Component } from "@prosopo/procaptcha-common";
17
- import type { PuzzleEvent } from "@prosopo/types";
18
- import { lightTheme } from "@prosopo/widget-skeleton";
19
- import {
20
- type Mock,
21
- afterEach,
22
- beforeEach,
23
- describe,
24
- expect,
25
- test,
26
- vi,
27
- } from "vitest";
28
- import {
29
- type PuzzleCanvasProps,
30
- mountPuzzleCanvas,
31
- } from "../components/puzzleCanvas.js";
32
-
33
- /**
34
- * The canvas is the only piece of the puzzle flow the user actually touches:
35
- * it owns the drag, the clamping that keeps the piece inside the board, and the
36
- * trail of positions the provider scores. Every test drives real DOM events
37
- * against a real render rather than calling the handlers directly.
38
- */
39
-
40
- const CONTAINER_WIDTH = 300;
41
- const CONTAINER_HEIGHT = 200;
42
- const PIECE_SIZE = 44;
43
-
44
- /**
45
- * The real translator reaches for an http backend the moment it is asked for a
46
- * string, which jsdom refuses. The English defaults the canvas ships stand in
47
- * instead, interpolated the way i18next would, so the assertions below read as
48
- * the copy a user is actually given.
49
- */
50
- const translator = (): Translator => ({
51
- t: (key: string, options?: Record<string, unknown>): string => {
52
- const template = (options?.defaultValue as string | undefined) ?? key;
53
- return template.replace(
54
- /{{(\w+)}}/g,
55
- (placeholder: string, name: string) =>
56
- options && name in options ? String(options[name]) : placeholder,
57
- );
58
- },
59
- isReady: () => true,
60
- subscribe: () => () => undefined,
61
- i18n: {} as Translator["i18n"],
62
- });
63
-
64
- let canvas: Component<PuzzleCanvasProps> | undefined;
65
- let onComplete: Mock<
66
- (finalX: number, finalY: number, puzzleEvents: PuzzleEvent[]) => void
67
- >;
68
-
69
- const props = (
70
- overrides: Partial<PuzzleCanvasProps> = {},
71
- ): PuzzleCanvasProps => ({
72
- originX: 20,
73
- originY: 100,
74
- background: "data:image/webp;base64,UklGRg==",
75
- piece: "data:image/webp;base64,UklGRg==",
76
- pieceSize: PIECE_SIZE,
77
- onComplete,
78
- showRetry: false,
79
- submitting: false,
80
- theme: lightTheme,
81
- translator: translator(),
82
- ...overrides,
83
- });
84
-
85
- const render = (canvasProps: PuzzleCanvasProps): void => {
86
- if (canvas) {
87
- canvas.update(canvasProps);
88
- } else {
89
- canvas = mountPuzzleCanvas(canvasProps);
90
- }
91
- };
92
-
93
- const destroy = (): void => {
94
- canvas?.destroy();
95
- canvas = undefined;
96
- };
97
-
98
- /**
99
- * The canvas puts itself on the body — it has to escape the query container
100
- * the widget skeleton wraps it in — so everything that reads the rendered
101
- * output reads the body.
102
- */
103
- const overlay = (): HTMLElement => document.body;
104
-
105
- const piece = (): HTMLElement => {
106
- const element = overlay().querySelector<HTMLElement>(
107
- '[data-cy="prosopo-puzzle-piece"]',
108
- );
109
- if (!element) throw new Error("expected the puzzle piece to be rendered");
110
- return element;
111
- };
112
-
113
- /** The piece is positioned by its centre, so undo the offset the style adds. */
114
- const piecePosition = (): { x: number; y: number } => ({
115
- x: Number.parseFloat(piece().style.left) + PIECE_SIZE / 2,
116
- y: Number.parseFloat(piece().style.top) + PIECE_SIZE / 2,
117
- });
118
-
119
- const mouseDown = (clientX: number, clientY: number): void => {
120
- piece().dispatchEvent(
121
- new MouseEvent("mousedown", { bubbles: true, clientX, clientY }),
122
- );
123
- };
124
-
125
- const mouseMove = (clientX: number, clientY: number): void => {
126
- document.dispatchEvent(
127
- new MouseEvent("mousemove", { bubbles: true, clientX, clientY }),
128
- );
129
- };
130
-
131
- const mouseUp = (): void => {
132
- document.dispatchEvent(new MouseEvent("mouseup", { bubbles: true }));
133
- };
134
-
135
- /**
136
- * jsdom has no touch constructors, so the event carries a plain object shaped
137
- * like the single Touch the component reads.
138
- */
139
- const touchEvent = (
140
- type: string,
141
- touches: { clientX: number; clientY: number }[],
142
- ): Event => {
143
- const event = new Event(type, { bubbles: true });
144
- Object.defineProperty(event, "touches", { value: touches });
145
- return event;
146
- };
147
-
148
- const touchStart = (touches: { clientX: number; clientY: number }[]): void => {
149
- piece().dispatchEvent(touchEvent("touchstart", touches));
150
- };
151
-
152
- const touchMove = (touches: { clientX: number; clientY: number }[]): void => {
153
- document.dispatchEvent(touchEvent("touchmove", touches));
154
- };
155
-
156
- const touchEnd = (): void => {
157
- document.dispatchEvent(touchEvent("touchend", []));
158
- };
159
-
160
- const keyDown = (key: string, options: { shiftKey?: boolean } = {}): void => {
161
- piece().dispatchEvent(
162
- new KeyboardEvent("keydown", {
163
- key,
164
- bubbles: true,
165
- cancelable: true,
166
- shiftKey: options.shiftKey ?? false,
167
- }),
168
- );
169
- };
170
-
171
- /**
172
- * `isTrusted` is unforgeable, so jsdom can only ever produce the synthetic
173
- * events the canvas refuses. Opening the allowance is how the firefox cypress
174
- * leg drives the widget too; the gate itself is covered by the one test that
175
- * shuts it again.
176
- */
177
- const allowSyntheticEvents = (allowed: boolean): void => {
178
- vi.stubGlobal("__PROSOPO_ALLOW_UNTRUSTED_EVENTS__", allowed);
179
- };
180
-
181
- const required = (element: HTMLElement | null, what: string): HTMLElement => {
182
- if (!element) throw new Error(`expected ${what} to be rendered`);
183
- return element;
184
- };
185
-
186
- const dialog = (): HTMLElement =>
187
- required(
188
- overlay().querySelector<HTMLElement>('[role="dialog"]'),
189
- "the dialog",
190
- );
191
-
192
- /** An `output` element carries the implicit `status` role a live region needs. */
193
- const liveRegion = (): HTMLElement =>
194
- required(overlay().querySelector<HTMLElement>("output"), "the live region");
195
-
196
- /** The text a screen reader reads out when the piece takes focus. */
197
- const pieceDescription = (): string =>
198
- (piece().getAttribute("aria-describedby") ?? "")
199
- .split(" ")
200
- .map((id) => document.getElementById(id)?.textContent ?? "")
201
- .join(" ");
202
-
203
- beforeEach(() => {
204
- allowSyntheticEvents(true);
205
- onComplete =
206
- vi.fn<
207
- (finalX: number, finalY: number, puzzleEvents: PuzzleEvent[]) => void
208
- >();
209
- canvas = undefined;
210
- });
211
-
212
- afterEach(() => {
213
- destroy();
214
- vi.useRealTimers();
215
- vi.unstubAllGlobals();
216
- vi.restoreAllMocks();
217
- });
218
-
219
- describe("what it puts on screen", () => {
220
- test("the piece starts at the origin the challenge named", () => {
221
- render(props());
222
- expect(piecePosition()).toEqual({ x: 20, y: 100 });
223
- });
224
-
225
- test("a new challenge moves the piece back to its new origin", () => {
226
- render(props());
227
- mouseDown(20, 100);
228
- mouseMove(120, 120);
229
- render(props({ originX: 50, originY: 60 }));
230
- expect(piecePosition()).toEqual({ x: 50, y: 60 });
231
- });
232
-
233
- test("re-rendering with the same origin leaves a dragged piece alone", () => {
234
- render(props());
235
- mouseDown(20, 100);
236
- mouseMove(120, 120);
237
- render(props());
238
- expect(piecePosition()).toEqual({ x: 120, y: 120 });
239
- });
240
-
241
- test("the first go asks the user to drag the piece", () => {
242
- render(props());
243
- expect(overlay().textContent).toContain("Drag the piece to the target");
244
- });
245
-
246
- test("a retry says so instead", () => {
247
- render(props({ showRetry: true }));
248
- expect(overlay().textContent).toContain("Not quite");
249
- });
250
-
251
- test("the piece cannot be grabbed while a solution is in flight", () => {
252
- render(props({ submitting: true }));
253
- mouseDown(20, 100);
254
- mouseMove(120, 120);
255
- mouseUp();
256
- expect(piecePosition()).toEqual({ x: 20, y: 100 });
257
- expect(onComplete).not.toHaveBeenCalled();
258
- });
259
-
260
- test("a tap cannot be started while a solution is in flight either", () => {
261
- render(props({ submitting: true }));
262
- touchStart([{ clientX: 20, clientY: 100 }]);
263
- touchMove([{ clientX: 120, clientY: 120 }]);
264
- touchEnd();
265
- expect(onComplete).not.toHaveBeenCalled();
266
- });
267
-
268
- test("the shake on a retry stops on its own", () => {
269
- vi.useFakeTimers();
270
- render(props({ showRetry: true }));
271
- vi.advanceTimersByTime(600);
272
- // Nothing to assert beyond survival: the timer fires into a live
273
- // component rather than leaking past the shake.
274
- expect(piecePosition()).toEqual({ x: 20, y: 100 });
275
- });
276
-
277
- test("unmounting mid-shake cancels the timer", () => {
278
- vi.useFakeTimers();
279
- render(props({ showRetry: true }));
280
- destroy();
281
- vi.advanceTimersByTime(600);
282
- expect(
283
- overlay().querySelector('[data-cy="prosopo-puzzle-piece"]'),
284
- ).toBeNull();
285
- });
286
- });
287
-
288
- describe("dragging with a mouse", () => {
289
- test("the piece follows the pointer", () => {
290
- render(props());
291
- mouseDown(20, 100);
292
- mouseMove(150, 90);
293
- expect(piecePosition()).toEqual({ x: 150, y: 90 });
294
- });
295
-
296
- test("grabbing the piece off-centre keeps the offset", () => {
297
- render(props());
298
- // Grabbed 5px right of the centre, so the centre trails the pointer by 5.
299
- mouseDown(25, 100);
300
- mouseMove(150, 100);
301
- expect(piecePosition()).toEqual({ x: 145, y: 100 });
302
- });
303
-
304
- test("a pointer moving before the piece is grabbed is ignored", () => {
305
- render(props());
306
- mouseMove(150, 90);
307
- expect(piecePosition()).toEqual({ x: 20, y: 100 });
308
- });
309
-
310
- test("the piece cannot be dragged off the left or top of the board", () => {
311
- render(props());
312
- mouseDown(20, 100);
313
- mouseMove(-500, -500);
314
- expect(piecePosition()).toEqual({ x: 0, y: 0 });
315
- });
316
-
317
- test("nor off the right or bottom", () => {
318
- render(props());
319
- mouseDown(20, 100);
320
- mouseMove(5000, 5000);
321
- expect(piecePosition()).toEqual({
322
- x: CONTAINER_WIDTH,
323
- y: CONTAINER_HEIGHT,
324
- });
325
- });
326
-
327
- test("letting go reports where the piece landed, with the trail", () => {
328
- render(props());
329
- mouseDown(20, 100);
330
- mouseMove(100, 95);
331
- mouseMove(200, 80);
332
- mouseUp();
333
- expect(onComplete).toHaveBeenCalledTimes(1);
334
- const [finalX, finalY, events] = onComplete.mock.calls[0] ?? [];
335
- expect(finalX).toBe(200);
336
- expect(finalY).toBe(80);
337
- expect(events?.map((event: PuzzleEvent) => [event.x, event.y])).toEqual([
338
- [100, 95],
339
- [200, 80],
340
- ]);
341
- });
342
-
343
- // Reporting the origin here spent the challenge on a position that cannot be
344
- // correct, and the provider rejects the replacement fetch — so a single
345
- // click that never moved ended the attempt outright.
346
- test("a grab released without moving reports nothing", () => {
347
- render(props());
348
- mouseDown(20, 100);
349
- mouseUp();
350
- expect(onComplete).not.toHaveBeenCalled();
351
- });
352
-
353
- test("letting go without having grabbed anything reports nothing", () => {
354
- render(props());
355
- mouseUp();
356
- expect(onComplete).not.toHaveBeenCalled();
357
- });
358
-
359
- test("a second release after the drop is ignored", () => {
360
- render(props());
361
- mouseDown(20, 100);
362
- mouseMove(200, 80);
363
- mouseUp();
364
- mouseUp();
365
- expect(onComplete).toHaveBeenCalledTimes(1);
366
- });
367
-
368
- test("moving after the drop no longer moves the piece", () => {
369
- render(props());
370
- mouseDown(20, 100);
371
- mouseMove(200, 80);
372
- mouseUp();
373
- mouseMove(50, 50);
374
- expect(piecePosition()).toEqual({ x: 200, y: 80 });
375
- });
376
-
377
- test("a second drag starts from a clean trail", () => {
378
- render(props());
379
- mouseDown(20, 100);
380
- mouseMove(100, 95);
381
- mouseUp();
382
- mouseDown(100, 95);
383
- mouseMove(200, 80);
384
- mouseUp();
385
- const events = onComplete.mock.calls[1]?.[2];
386
- expect(events?.map((event: PuzzleEvent) => [event.x, event.y])).toEqual([
387
- [200, 80],
388
- ]);
389
- });
390
-
391
- test("the trail is timestamped in order", () => {
392
- render(props());
393
- mouseDown(20, 100);
394
- mouseMove(100, 95);
395
- mouseMove(200, 80);
396
- mouseUp();
397
- const events = onComplete.mock.calls[0]?.[2] ?? [];
398
- expect(events).toHaveLength(2);
399
- expect(events[1]?.t).toBeGreaterThanOrEqual(events[0]?.t ?? 0);
400
- });
401
-
402
- test("the trail handed over is a copy, safe from the next drag", () => {
403
- render(props());
404
- mouseDown(20, 100);
405
- mouseMove(200, 80);
406
- mouseUp();
407
- const events = onComplete.mock.calls[0]?.[2] ?? [];
408
- mouseDown(200, 80);
409
- mouseMove(10, 10);
410
- expect(events).toHaveLength(1);
411
- });
412
- });
413
-
414
- describe("dragging with a finger", () => {
415
- test("the piece follows the touch", () => {
416
- render(props());
417
- touchStart([{ clientX: 20, clientY: 100 }]);
418
- touchMove([{ clientX: 150, clientY: 90 }]);
419
- expect(piecePosition()).toEqual({ x: 150, y: 90 });
420
- });
421
-
422
- test("lifting the finger reports the drop", () => {
423
- render(props());
424
- touchStart([{ clientX: 20, clientY: 100 }]);
425
- touchMove([{ clientX: 200, clientY: 80 }]);
426
- touchEnd();
427
- expect(onComplete).toHaveBeenCalledWith(200, 80, [
428
- expect.objectContaining({ x: 200, y: 80 }),
429
- ]);
430
- });
431
-
432
- test("a touchstart carrying no touches does not start a drag", () => {
433
- render(props());
434
- touchStart([]);
435
- touchMove([{ clientX: 150, clientY: 90 }]);
436
- touchEnd();
437
- expect(piecePosition()).toEqual({ x: 20, y: 100 });
438
- expect(onComplete).not.toHaveBeenCalled();
439
- });
440
-
441
- // The panel opens centred, under the finger that just pressed the checkbox,
442
- // so tapping the piece instead of dragging it is the easy mistake to make on
443
- // a phone. It used to submit the untouched origin, which fails, and the
444
- // widget then reset — indistinguishable to the user from the challenge
445
- // never having worked.
446
- test("tapping the piece without dragging reports nothing", () => {
447
- render(props());
448
- touchStart([{ clientX: 20, clientY: 100 }]);
449
- touchEnd();
450
- expect(onComplete).not.toHaveBeenCalled();
451
- expect(piecePosition()).toEqual({ x: 20, y: 100 });
452
- });
453
-
454
- test("a touchmove carrying no touches is ignored mid-drag", () => {
455
- render(props());
456
- touchStart([{ clientX: 20, clientY: 100 }]);
457
- touchMove([]);
458
- expect(piecePosition()).toEqual({ x: 20, y: 100 });
459
- });
460
-
461
- test("a finger drag is clamped to the board like a mouse drag", () => {
462
- render(props());
463
- touchStart([{ clientX: 20, clientY: 100 }]);
464
- touchMove([{ clientX: 9999, clientY: -9999 }]);
465
- expect(piecePosition()).toEqual({ x: CONTAINER_WIDTH, y: 0 });
466
- });
467
- });
468
-
469
- describe("driving it from the keyboard", () => {
470
- test("the piece takes focus as soon as the puzzle opens", () => {
471
- render(props());
472
- expect(document.activeElement).toBe(piece());
473
- });
474
-
475
- test("an arrow key walks the piece a step across the board", () => {
476
- render(props());
477
- keyDown("ArrowRight");
478
- expect(piecePosition()).toEqual({ x: 30, y: 100 });
479
- });
480
-
481
- test("holding shift walks it a finer step", () => {
482
- render(props());
483
- keyDown("ArrowRight", { shiftKey: true });
484
- expect(piecePosition()).toEqual({ x: 22, y: 100 });
485
- });
486
-
487
- test("all four arrows move the piece the way they point", () => {
488
- render(props());
489
- keyDown("ArrowRight");
490
- keyDown("ArrowDown");
491
- keyDown("ArrowLeft");
492
- keyDown("ArrowUp");
493
- expect(piecePosition()).toEqual({ x: 20, y: 100 });
494
- });
495
-
496
- test("the board's edge stops the piece just as a drag does", () => {
497
- render(props());
498
- for (let press = 0; press < 5; press++) keyDown("ArrowLeft");
499
- expect(piecePosition()).toEqual({ x: 0, y: 100 });
500
- });
501
-
502
- test("home puts the piece back where it started", () => {
503
- render(props());
504
- keyDown("ArrowRight");
505
- keyDown("ArrowDown");
506
- keyDown("Home");
507
- expect(piecePosition()).toEqual({ x: 20, y: 100 });
508
- });
509
-
510
- test("enter hands over where the piece is, with the trail", () => {
511
- render(props());
512
- keyDown("ArrowRight");
513
- keyDown("ArrowRight");
514
- keyDown("Enter");
515
- expect(onComplete).toHaveBeenCalledTimes(1);
516
- const [finalX, finalY, events] = onComplete.mock.calls[0] ?? [];
517
- expect(finalX).toBe(40);
518
- expect(finalY).toBe(100);
519
- expect(events?.map((event) => [event.x, event.y])).toEqual([
520
- [30, 100],
521
- [40, 100],
522
- ]);
523
- });
524
-
525
- test("space submits too", () => {
526
- render(props());
527
- keyDown("ArrowRight");
528
- keyDown(" ");
529
- expect(onComplete).toHaveBeenCalledWith(30, 100, [
530
- expect.objectContaining({ x: 30, y: 100 }),
531
- ]);
532
- });
533
-
534
- test("a second keyboard go starts from a clean trail", () => {
535
- render(props());
536
- keyDown("ArrowRight");
537
- keyDown("Enter");
538
- keyDown("ArrowDown");
539
- keyDown("Enter");
540
- const events = onComplete.mock.calls[1]?.[2];
541
- expect(events?.map((event) => [event.x, event.y])).toEqual([[30, 110]]);
542
- });
543
-
544
- test("a key press no user made is ignored", () => {
545
- allowSyntheticEvents(false);
546
- render(props());
547
- keyDown("ArrowRight");
548
- keyDown("Enter");
549
- expect(piecePosition()).toEqual({ x: 20, y: 100 });
550
- expect(onComplete).not.toHaveBeenCalled();
551
- });
552
-
553
- test("keys that mean nothing here are left to the page", () => {
554
- render(props());
555
- keyDown("a");
556
- expect(piecePosition()).toEqual({ x: 20, y: 100 });
557
- expect(onComplete).not.toHaveBeenCalled();
558
- });
559
-
560
- test("the piece cannot be moved or submitted while a solution is in flight", () => {
561
- render(props({ submitting: true }));
562
- keyDown("ArrowRight");
563
- keyDown("Enter");
564
- expect(piecePosition()).toEqual({ x: 20, y: 100 });
565
- expect(onComplete).not.toHaveBeenCalled();
566
- });
567
-
568
- test("nor can it be tabbed to while a solution is in flight", () => {
569
- render(props({ submitting: true }));
570
- expect(piece().tabIndex).toBe(-1);
571
- });
572
-
573
- test("tab cannot leave the dialog for the page behind it", () => {
574
- render(props());
575
- document.dispatchEvent(
576
- new KeyboardEvent("keydown", { key: "Tab", bubbles: true }),
577
- );
578
- expect(document.activeElement).toBe(piece());
579
- });
580
-
581
- test("closing hands focus back to whatever opened the puzzle", () => {
582
- const opener = document.createElement("button");
583
- document.body.appendChild(opener);
584
- opener.focus();
585
-
586
- render(props());
587
- expect(document.activeElement).toBe(piece());
588
-
589
- destroy();
590
- expect(document.activeElement).toBe(opener);
591
-
592
- opener.remove();
593
- });
594
- });
595
-
596
- describe("what it says to a screen reader", () => {
597
- test("the panel announces itself as a named dialog", () => {
598
- render(props());
599
- expect(dialog().getAttribute("aria-label")).toBe("Puzzle challenge");
600
- expect(dialog().getAttribute("aria-modal")).toBe("true");
601
- });
602
-
603
- test("the piece carries a name and says it can be dragged", () => {
604
- render(props());
605
- expect(piece().getAttribute("aria-label")).toBe("Puzzle piece");
606
- expect(piece().getAttribute("aria-roledescription")).toBe(
607
- "draggable puzzle piece",
608
- );
609
- });
610
-
611
- test("the piece is described by the instruction and the key help", () => {
612
- render(props());
613
- expect(pieceDescription()).toContain("Drag the piece to the target");
614
- expect(pieceDescription()).toContain("arrow keys");
615
- expect(pieceDescription()).toContain("Escape");
616
- });
617
-
618
- test("taking focus reports where the piece is, in proportions", () => {
619
- render(props());
620
- expect(liveRegion().textContent).toBe("7 percent across, 50 percent down");
621
- });
622
-
623
- test("a move is reported once the keys stop, not on every press", () => {
624
- vi.useFakeTimers();
625
- render(props());
626
- keyDown("ArrowRight");
627
- keyDown("ArrowRight");
628
- expect(liveRegion().textContent).toBe("7 percent across, 50 percent down");
629
- vi.advanceTimersByTime(500);
630
- expect(liveRegion().textContent).toBe("13 percent across, 50 percent down");
631
- });
632
-
633
- test("a solution in flight is announced", () => {
634
- render(props({ submitting: true }));
635
- expect(liveRegion().textContent).toBe("Checking your answer");
636
- });
637
-
638
- test("a retry says the puzzle has been replaced", () => {
639
- render(props({ showRetry: true }));
640
- expect(liveRegion().textContent).toContain("A new puzzle has loaded");
641
- });
642
-
643
- test("the background tiles are left out of the reading order", () => {
644
- render(props());
645
- const images = Array.from(overlay().querySelectorAll("img"));
646
- expect(images.length).toBeGreaterThan(0);
647
- expect(images.every((image) => image.getAttribute("alt") === "")).toBe(
648
- true,
649
- );
650
- });
651
- });
652
-
653
- describe("after it goes away", () => {
654
- test("its document listeners go with it", () => {
655
- render(props());
656
- mouseDown(20, 100);
657
- destroy();
658
- mouseMove(150, 90);
659
- mouseUp();
660
- expect(onComplete).not.toHaveBeenCalled();
661
- });
662
- });
package/tsconfig.cjs.json DELETED
@@ -1,43 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.cjs.json",
3
- "compilerOptions": {
4
- "rootDir": "./src",
5
- "outDir": "./dist/cjs",
6
- "lib": ["es6", "dom"]
7
- },
8
- "include": [
9
- "./src/**/*.ts",
10
- "./src/**/*.json",
11
- "./src/**/*.d.ts",
12
- "./src/**/*.tsx"
13
- ],
14
- "references": [
15
- {
16
- "path": "../../dev/config/tsconfig.cjs.json"
17
- },
18
- {
19
- "path": "../api/tsconfig.cjs.json"
20
- },
21
- {
22
- "path": "../common/tsconfig.cjs.json"
23
- },
24
- {
25
- "path": "../locale/tsconfig.cjs.json"
26
- },
27
- {
28
- "path": "../procaptcha-common/tsconfig.cjs.json"
29
- },
30
- {
31
- "path": "../types/tsconfig.cjs.json"
32
- },
33
- {
34
- "path": "../util/tsconfig.cjs.json"
35
- },
36
- {
37
- "path": "../util-crypto/tsconfig.cjs.json"
38
- },
39
- {
40
- "path": "../widget-skeleton/tsconfig.cjs.json"
41
- }
42
- ]
43
- }