| 1 | // Run: tsx src/__tests__/notification-volume-slider.test.tsx |
| 2 | |
| 3 | import React, { act } from "react"; |
| 4 | import { JSDOM } from "jsdom"; |
| 5 | |
| 6 | let passed = 0; |
| 7 | let failed = 0; |
| 8 | function ok(value: boolean, label: string) { |
| 9 | if (value) { |
| 10 | process.stdout.write(` PASS ${label}\n`); |
| 11 | passed += 1; |
| 12 | } else { |
| 13 | process.stdout.write(` FAIL ${label}\n`); |
| 14 | failed += 1; |
| 15 | } |
| 16 | } |
| 17 | |
| 18 | console.log("\nnotification volume slider"); |
| 19 | const dom = new JSDOM('<!doctype html><html><body><div id="root"></div></body></html>', { |
| 20 | pretendToBeVisual: true, |
| 21 | url: "http://localhost/", |
| 22 | }); |
| 23 | (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; |
| 24 | globalThis.window = dom.window as unknown as Window & typeof globalThis; |
| 25 | globalThis.document = dom.window.document; |
| 26 | Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator }); |
| 27 | globalThis.HTMLElement = dom.window.HTMLElement; |
| 28 | globalThis.Event = dom.window.Event; |
| 29 | |
| 30 | const [{ createRoot }, { NotificationVolumeSlider }, { LocaleProvider }] = await Promise.all([ |
| 31 | import("react-dom/client"), |
| 32 | import("../components/NotificationVolumeSlider"), |
| 33 | import("../lib/i18n"), |
| 34 | ]); |
| 35 | |
| 36 | const rootElement = document.getElementById("root"); |
| 37 | if (!rootElement) throw new Error("missing root"); |
| 38 | const root = createRoot(rootElement); |
| 39 | const changes: number[] = []; |
| 40 | await act(async () => { |
| 41 | root.render( |
| 42 | <LocaleProvider> |
| 43 | <NotificationVolumeSlider value={70} onChange={(value) => changes.push(value)} /> |
| 44 | </LocaleProvider>, |
| 45 | ); |
| 46 | }); |
| 47 | |
| 48 | const input = document.querySelector<HTMLInputElement>('input[type="range"]'); |
| 49 | const output = document.querySelector<HTMLOutputElement>("output"); |
| 50 | ok(input?.getAttribute("aria-label") === "Notification volume", "slider exposes its localized purpose"); |
| 51 | ok(input?.min === "0" && input?.max === "100" && input?.step === "5", "slider exposes the full 0–100% keyboard range"); |
| 52 | ok(input?.value === "70" && input?.getAttribute("aria-valuetext") === "70%", "slider announces the default percentage"); |
| 53 | ok(output?.textContent === "70%" && output?.getAttribute("for") === input?.id, "visible percentage is associated with the slider"); |
| 54 | await act(async () => { |
| 55 | if (!input) return; |
| 56 | const setter = Object.getOwnPropertyDescriptor(dom.window.HTMLInputElement.prototype, "value")?.set; |
| 57 | setter?.call(input, "85"); |
| 58 | input.dispatchEvent(new dom.window.Event("input", { bubbles: true })); |
| 59 | input.dispatchEvent(new dom.window.Event("change", { bubbles: true })); |
| 60 | }); |
| 61 | ok(changes.at(-1) === 85, "dragging or using the keyboard emits the next percentage"); |
| 62 | |
| 63 | await act(async () => root.unmount()); |
| 64 | dom.window.close(); |
| 65 | process.stdout.write(`\n${passed} passed, ${failed} failed\n`); |
| 66 | if (failed > 0) process.exit(1); |
| 67 |