diff --git a/docs/qa.md b/docs/qa.md index ee124d6c..8dc58351 100644 --- a/docs/qa.md +++ b/docs/qa.md @@ -4,6 +4,12 @@ Run this checklist before publishing releases or merging changes that touch uplo Before starting, open `docs/kitchen-sink.html` in a browser to review the modal surface reference. With that page as your visual baseline, perform this quick accessibility spot check in the running app: +Automated regression coverage now exercises these flows across both the legacy and design-system variants: + +* `tests/video-modal-accessibility.test.mjs` validates sticky header transitions, scroll locking, and zap popover accessibility for the video modal. +* `tests/modal-accessibility.test.mjs` mirrors upload/edit modal accessibility along with their form validation and mode toggles. +* `tests/profile-modal-controller.test.mjs` verifies profile modal focus restoration and navigation button state changes. + * Launch the site, trigger any video card or thumbnail to open the playback modal, and keep track of the element you used to open it. * Scroll the modal content down and back up; confirm the sticky navigation bar hides on downward scroll and reappears when you reverse direction. * Activate the zap button to display the popover and tab through its fields to confirm focus stays within the dialog; press `Escape` to close it and ensure focus returns to the zap trigger. diff --git a/tests/modal-accessibility.test.mjs b/tests/modal-accessibility.test.mjs index 53efe00e..a70928d0 100644 --- a/tests/modal-accessibility.test.mjs +++ b/tests/modal-accessibility.test.mjs @@ -11,6 +11,11 @@ import { openStaticModal, closeStaticModal, } from "../js/ui/components/staticModalAccessibility.js"; +import { + setFeatureDesignSystemEnabled, + resetRuntimeFlags, +} from "../js/constants.js"; +import { applyDesignSystemAttributes } from "../js/designSystem.js"; const uploadMarkupPromise = readFile( new URL("../components/upload-modal.html", import.meta.url), @@ -90,101 +95,388 @@ async function flushMicrotasks() { await Promise.resolve(); } -test("UploadModal closes on Escape and restores trigger focus", async () => { - const markup = await uploadMarkupPromise; - const cleanups = []; - cleanups.push( - installDom( - "
", - ), - ); - cleanups.push( - stubFetch( - new Map([["components/upload-modal.html", markup]]), - ), - ); +for (const designSystemEnabled of [false, true]) { + const modeLabel = designSystemEnabled ? "design-system" : "legacy"; - const modal = new UploadModal({ - removeTrackingScripts: () => {}, - setGlobalModalState: () => {}, - }); - await modal.load(); + test( + `[${modeLabel}] UploadModal closes on Escape and restores trigger focus`, + async (t) => { + const markup = await uploadMarkupPromise; + const cleanups = []; + cleanups.push( + installDom( + "
", + ), + ); + cleanups.push( + stubFetch(new Map([["components/upload-modal.html", markup]])), + ); - const trigger = document.getElementById("trigger"); - assert.ok(trigger); + setFeatureDesignSystemEnabled(designSystemEnabled); - modal.open({ triggerElement: trigger }); - await flushMicrotasks(); + const modal = new UploadModal({ + removeTrackingScripts: () => {}, + setGlobalModalState: () => {}, + }); + await modal.load(); + applyDesignSystemAttributes(document); - document.dispatchEvent( - new window.KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }), - ); - await flushMicrotasks(); + const trigger = document.getElementById("trigger"); + assert.ok(trigger); - const root = modal.getRoot(); - assert.ok(root?.classList.contains("hidden")); - assert.strictEqual(document.activeElement, trigger); + modal.open({ triggerElement: trigger }); + await flushMicrotasks(); - modal.destroy(); - cleanups.reverse().forEach((fn) => fn?.()); -}); + document.dispatchEvent( + new window.KeyboardEvent("keydown", { + key: "Escape", + bubbles: true, + cancelable: true, + }), + ); + await flushMicrotasks(); -test("EditModal Escape closes and restores trigger focus", async () => { - const markup = await editMarkupPromise; - const cleanups = []; - cleanups.push( - installDom( - "
", - ), - ); - cleanups.push( - stubFetch( - new Map([["components/edit-video-modal.html", markup]]), - ), - ); + const root = modal.getRoot(); + assert.ok(root?.classList.contains("hidden")); + assert.strictEqual(document.activeElement, trigger); - const modal = new EditModal({ - removeTrackingScripts: () => {}, - setGlobalModalState: () => {}, - sanitizers: {}, - escapeHtml: (value) => String(value ?? ""), - showError: () => {}, - getMode: () => "live", - }); - await modal.load(); - - const trigger = document.getElementById("trigger"); - assert.ok(trigger); - - await modal.open( - { - id: "event1", - title: "Example", - url: "https://example.com/video.mp4", - magnet: "", - ws: "", - xs: "", - enableComments: true, - isPrivate: false, - isNsfw: false, - isForKids: false, + t.after(() => { + modal.destroy(); + resetRuntimeFlags(); + cleanups.reverse().forEach((fn) => fn?.()); + }); }, - { triggerElement: trigger }, ); - await flushMicrotasks(); - document.dispatchEvent( - new window.KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }), + test( + `[${modeLabel}] UploadModal backdrop click closes and restores trigger focus`, + async (t) => { + const markup = await uploadMarkupPromise; + const cleanups = []; + cleanups.push( + installDom( + "
", + ), + ); + cleanups.push( + stubFetch(new Map([["components/upload-modal.html", markup]])), + ); + + setFeatureDesignSystemEnabled(designSystemEnabled); + + const modal = new UploadModal({ + removeTrackingScripts: () => {}, + setGlobalModalState: () => {}, + }); + await modal.load(); + applyDesignSystemAttributes(document); + + const trigger = document.getElementById("trigger"); + assert.ok(trigger); + + modal.open({ triggerElement: trigger }); + await flushMicrotasks(); + + const backdrop = modal.getRoot()?.querySelector?.("[data-dismiss]"); + assert.ok(backdrop, "expected upload modal backdrop"); + + backdrop.dispatchEvent( + new window.MouseEvent("click", { bubbles: true, cancelable: true }), + ); + await flushMicrotasks(); + + const root = modal.getRoot(); + assert.ok(root?.classList.contains("hidden")); + assert.strictEqual(document.activeElement, trigger); + + t.after(() => { + modal.destroy(); + resetRuntimeFlags(); + cleanups.reverse().forEach((fn) => fn?.()); + }); + }, ); - await flushMicrotasks(); - const root = modal.getRoot(); - assert.ok(root?.classList.contains("hidden")); - assert.strictEqual(document.activeElement, trigger); + test( + `[${modeLabel}] UploadModal mode toggle updates button states`, + async (t) => { + const markup = await uploadMarkupPromise; + const cleanups = []; + cleanups.push( + installDom( + "
", + ), + ); + cleanups.push( + stubFetch(new Map([["components/upload-modal.html", markup]])), + ); - modal.destroy(); - cleanups.reverse().forEach((fn) => fn?.()); -}); + setFeatureDesignSystemEnabled(designSystemEnabled); + + const modal = new UploadModal({ + removeTrackingScripts: () => {}, + setGlobalModalState: () => {}, + }); + await modal.load(); + applyDesignSystemAttributes(document); + + const customButton = document.querySelector( + ".upload-mode-toggle[data-upload-mode=\"custom\"]", + ); + const cloudflareButton = document.querySelector( + ".upload-mode-toggle[data-upload-mode=\"cloudflare\"]", + ); + const customSection = document.getElementById("customUploadSection"); + const cloudflareSection = document.getElementById("cloudflareUploadSection"); + + assert.ok(customButton && cloudflareButton); + assert.ok(customSection && cloudflareSection); + + modal.setMode("cloudflare"); + + assert.equal(cloudflareButton.getAttribute("aria-pressed"), "true"); + assert.equal(customButton.getAttribute("aria-pressed"), "false"); + assert.equal(cloudflareSection.classList.contains("hidden"), false); + assert.equal(customSection.classList.contains("hidden"), true); + + modal.setMode("custom"); + + assert.equal(customButton.getAttribute("aria-pressed"), "true"); + assert.equal(cloudflareButton.getAttribute("aria-pressed"), "false"); + assert.equal(customSection.classList.contains("hidden"), false); + assert.equal(cloudflareSection.classList.contains("hidden"), true); + + t.after(() => { + modal.destroy(); + resetRuntimeFlags(); + cleanups.reverse().forEach((fn) => fn?.()); + }); + }, + ); +} + +for (const designSystemEnabled of [false, true]) { + const modeLabel = designSystemEnabled ? "design-system" : "legacy"; + + test( + `[${modeLabel}] EditModal Escape closes and restores trigger focus`, + async (t) => { + const markup = await editMarkupPromise; + const cleanups = []; + cleanups.push( + installDom( + "
", + ), + ); + cleanups.push( + stubFetch( + new Map([["components/edit-video-modal.html", markup]]), + ), + ); + + setFeatureDesignSystemEnabled(designSystemEnabled); + + const modal = new EditModal({ + removeTrackingScripts: () => {}, + setGlobalModalState: () => {}, + sanitizers: {}, + escapeHtml: (value) => String(value ?? ""), + showError: () => {}, + getMode: () => "live", + }); + await modal.load(); + applyDesignSystemAttributes(document); + + const trigger = document.getElementById("trigger"); + assert.ok(trigger); + + await modal.open( + { + id: "event1", + title: "Example", + url: "https://example.com/video.mp4", + magnet: "", + ws: "", + xs: "", + enableComments: true, + isPrivate: false, + isNsfw: false, + isForKids: false, + }, + { triggerElement: trigger }, + ); + await flushMicrotasks(); + + document.dispatchEvent( + new window.KeyboardEvent("keydown", { + key: "Escape", + bubbles: true, + cancelable: true, + }), + ); + await flushMicrotasks(); + + const root = modal.getRoot(); + assert.ok(root?.classList.contains("hidden")); + assert.strictEqual(document.activeElement, trigger); + + t.after(() => { + modal.destroy(); + resetRuntimeFlags(); + cleanups.reverse().forEach((fn) => fn?.()); + }); + }, + ); + + test( + `[${modeLabel}] EditModal backdrop click closes and restores trigger focus`, + async (t) => { + const markup = await editMarkupPromise; + const cleanups = []; + cleanups.push( + installDom( + "
", + ), + ); + cleanups.push( + stubFetch( + new Map([["components/edit-video-modal.html", markup]]), + ), + ); + + setFeatureDesignSystemEnabled(designSystemEnabled); + + const modal = new EditModal({ + removeTrackingScripts: () => {}, + setGlobalModalState: () => {}, + sanitizers: {}, + escapeHtml: (value) => String(value ?? ""), + showError: () => {}, + getMode: () => "live", + }); + await modal.load(); + applyDesignSystemAttributes(document); + + const trigger = document.getElementById("trigger"); + assert.ok(trigger); + + await modal.open( + { + id: "event1", + title: "Example", + url: "https://example.com/video.mp4", + magnet: "", + ws: "", + xs: "", + enableComments: true, + isPrivate: false, + isNsfw: false, + isForKids: false, + }, + { triggerElement: trigger }, + ); + await flushMicrotasks(); + + const overlay = modal.overlay; + assert.ok(overlay, "expected edit modal overlay"); + + overlay.dispatchEvent( + new window.MouseEvent("click", { bubbles: true, cancelable: true }), + ); + await flushMicrotasks(); + + const root = modal.getRoot(); + assert.ok(root?.classList.contains("hidden")); + assert.strictEqual(document.activeElement, trigger); + + t.after(() => { + modal.destroy(); + resetRuntimeFlags(); + cleanups.reverse().forEach((fn) => fn?.()); + }); + }, + ); + + test( + `[${modeLabel}] EditModal visibility toggle updates button state`, + async (t) => { + const markup = await editMarkupPromise; + const cleanups = []; + cleanups.push( + installDom( + "
", + ), + ); + cleanups.push( + stubFetch( + new Map([["components/edit-video-modal.html", markup]]), + ), + ); + + setFeatureDesignSystemEnabled(designSystemEnabled); + + const modal = new EditModal({ + removeTrackingScripts: () => {}, + setGlobalModalState: () => {}, + sanitizers: {}, + escapeHtml: (value) => String(value ?? ""), + showError: () => {}, + getMode: () => "live", + }); + await modal.load(); + applyDesignSystemAttributes(document); + + await modal.open( + { + id: "event1", + title: "Example", + url: "https://example.com/video.mp4", + magnet: "", + ws: "", + xs: "", + enableComments: true, + isPrivate: false, + isNsfw: false, + isForKids: false, + }, + {}, + ); + await flushMicrotasks(); + + const privateButton = modal.visibility.buttons.find((button) => + button?.dataset?.visibilityOption === "private", + ); + const publicButton = modal.visibility.buttons.find((button) => + button?.dataset?.visibilityOption === "public", + ); + assert.ok(privateButton && publicButton); + + const unlockVisibilityButton = + modal.root?.querySelector("[data-edit-target=\"editVideoIsPrivate\"]"); + assert.ok(unlockVisibilityButton); + + unlockVisibilityButton.dispatchEvent( + new window.Event("click", { bubbles: true, cancelable: true }), + ); + + modal.setVisibility("private"); + assert.equal(privateButton.getAttribute("aria-pressed"), "true"); + assert.equal(publicButton.getAttribute("aria-pressed"), "false"); + assert.equal(modal.visibility.container?.dataset.state, "private"); + + modal.setVisibility("public"); + assert.equal(publicButton.getAttribute("aria-pressed"), "true"); + assert.equal(privateButton.getAttribute("aria-pressed"), "false"); + assert.equal(modal.visibility.container?.dataset.state, "public"); + + t.after(() => { + modal.destroy(); + resetRuntimeFlags(); + cleanups.reverse().forEach((fn) => fn?.()); + }); + }, + ); +} test("RevertModal Escape closes and restores trigger focus", async () => { const markup = await revertMarkupPromise; diff --git a/tests/profile-modal-controller.test.mjs b/tests/profile-modal-controller.test.mjs index f41b8579..3ec2ba73 100644 --- a/tests/profile-modal-controller.test.mjs +++ b/tests/profile-modal-controller.test.mjs @@ -3,6 +3,11 @@ import { strict as assert } from 'node:assert'; import { readFile } from 'node:fs/promises'; import { JSDOM } from 'jsdom'; import ProfileModalController from '../js/ui/profileModalController.js'; +import { + setFeatureDesignSystemEnabled, + resetRuntimeFlags, +} from '../js/constants.js'; +import { applyDesignSystemAttributes } from '../js/designSystem.js'; const profileModalHtml = await readFile( new URL('../components/profile-modal.html', import.meta.url), @@ -15,6 +20,12 @@ const defaultActorHex = 'a'.repeat(64); let dom; let container; +async function waitForAnimationFrame(window, cycles = 1) { + for (let i = 0; i < cycles; i += 1) { + await new Promise((resolve) => window.requestAnimationFrame(() => resolve())); + } +} + beforeEach(() => { dom = new JSDOM( '
', @@ -163,6 +174,129 @@ function createController(options = {}) { }); } +for (const designSystemEnabled of [false, true]) { + const modeLabel = designSystemEnabled ? 'design-system' : 'legacy'; + + test( + `[${modeLabel}] Profile modal Escape closes and restores trigger focus`, + async (t) => { + setFeatureDesignSystemEnabled(designSystemEnabled); + const controller = createController(); + await controller.load(); + applyDesignSystemAttributes(document); + + const trigger = document.createElement('button'); + trigger.id = 'profileTrigger'; + trigger.textContent = 'Open profile'; + document.body.appendChild(trigger); + trigger.focus(); + await waitForAnimationFrame(window, 1); + + const cleanup = () => { + try { + controller.hide({ silent: true }); + } catch {} + try { + trigger.remove(); + } catch {} + }; + + let cleanupRan = false; + try { + await controller.show('account'); + await waitForAnimationFrame(window, 2); + + const focusTrap = + controller.focusTrapContainer || + controller.profileModalPanel || + controller.profileModal || + document.getElementById('profileModal'); + assert.ok(focusTrap, 'focus trap container should exist'); + + focusTrap.dispatchEvent( + new window.KeyboardEvent('keydown', { + key: 'Escape', + bubbles: true, + cancelable: true, + }), + ); + + await waitForAnimationFrame(window, 2); + + const modalRoot = document.getElementById('profileModal'); + assert.ok(modalRoot?.classList.contains('hidden')); + assert.strictEqual(document.activeElement, trigger); + } finally { + cleanup(); + cleanupRan = true; + } + + t.after(() => { + if (!cleanupRan) { + cleanup(); + } + resetRuntimeFlags(); + }); + }, + ); + + test( + `[${modeLabel}] Profile modal navigation buttons toggle active state`, + async (t) => { + setFeatureDesignSystemEnabled(designSystemEnabled); + const controller = createController(); + await controller.load(); + applyDesignSystemAttributes(document); + + let cleanupRan = false; + const cleanup = () => { + try { + controller.hide({ silent: true }); + } catch {} + cleanupRan = true; + }; + + try { + await controller.show('account'); + await waitForAnimationFrame(window, 2); + + const accountButton = document.getElementById('profileNavAccount'); + const relaysButton = document.getElementById('profileNavRelays'); + const accountPane = document.getElementById('profilePaneAccount'); + const relaysPane = document.getElementById('profilePaneRelays'); + + assert.ok(accountButton && relaysButton); + assert.ok(accountPane && relaysPane); + + controller.selectPane('relays'); + await waitForAnimationFrame(window, 1); + + assert.equal(relaysButton?.getAttribute('aria-selected'), 'true'); + assert.equal(accountButton?.getAttribute('aria-selected'), 'false'); + assert.equal(relaysPane?.classList.contains('hidden'), false); + assert.equal(accountPane?.classList.contains('hidden'), true); + + controller.selectPane('account'); + await waitForAnimationFrame(window, 1); + + assert.equal(accountButton?.getAttribute('aria-selected'), 'true'); + assert.equal(relaysButton?.getAttribute('aria-selected'), 'false'); + assert.equal(accountPane?.classList.contains('hidden'), false); + assert.equal(relaysPane?.classList.contains('hidden'), true); + } finally { + cleanup(); + } + + t.after(() => { + if (!cleanupRan) { + cleanup(); + } + resetRuntimeFlags(); + }); + }, + ); +} + test('load() injects markup and caches expected elements', async () => { const controller = createController(); const result = await controller.load(); diff --git a/tests/video-modal-accessibility.test.mjs b/tests/video-modal-accessibility.test.mjs index 729eeb1a..f2144d03 100644 --- a/tests/video-modal-accessibility.test.mjs +++ b/tests/video-modal-accessibility.test.mjs @@ -4,17 +4,22 @@ import { readFile } from "node:fs/promises"; import { JSDOM } from "jsdom"; import { VideoModal } from "../js/ui/components/VideoModal.js"; +import { + setFeatureDesignSystemEnabled, + resetRuntimeFlags, +} from "../js/constants.js"; +import { applyDesignSystemAttributes } from "../js/designSystem.js"; const modalMarkupPromise = readFile( new URL("../components/video-modal.html", import.meta.url), "utf8" ); -async function setupModal() { +async function setupModal({ designSystemEnabled = false } = {}) { const markup = await modalMarkupPromise; const dom = new JSDOM( `
${markup}
`, - { url: "https://example.com" } + { url: "https://example.com", pretendToBeVisual: true } ); const { window } = dom; @@ -31,6 +36,11 @@ async function setupModal() { globalThis.EventTarget = window.EventTarget; globalThis.navigator = window.navigator; globalThis.location = window.location; + globalThis.KeyboardEvent = window.KeyboardEvent; + globalThis.MouseEvent = window.MouseEvent; + + setFeatureDesignSystemEnabled(designSystemEnabled); + applyDesignSystemAttributes(document); const modal = new VideoModal({ removeTrackingScripts: () => {}, @@ -42,15 +52,40 @@ async function setupModal() { const playerModal = document.querySelector("#playerModal"); assert.ok(playerModal, "player modal markup should exist"); modal.hydrate(playerModal); + applyDesignSystemAttributes(playerModal); const trigger = document.getElementById("trigger"); assert.ok(trigger, "trigger button should exist"); - return { window, document, modal, playerModal, trigger }; + const cleanup = () => { + try { + resetRuntimeFlags(); + } catch (error) { + console.warn("[tests] failed to reset runtime flags", error); + } + dom.window.close(); + delete globalThis.window; + delete globalThis.document; + delete globalThis.HTMLElement; + delete globalThis.HTMLVideoElement; + delete globalThis.Element; + delete globalThis.CustomEvent; + delete globalThis.Event; + delete globalThis.Node; + delete globalThis.EventTarget; + delete globalThis.navigator; + delete globalThis.location; + delete globalThis.KeyboardEvent; + delete globalThis.MouseEvent; + }; + + return { window, document, modal, playerModal, trigger, cleanup }; } -test("backdrop data-dismiss closes the modal and restores focus", async () => { - const { window, document, modal, playerModal, trigger } = await setupModal(); +test("backdrop data-dismiss closes the modal and restores focus", async (t) => { + const { window, document, modal, playerModal, trigger, cleanup } = + await setupModal(); + t.after(cleanup); const backdrop = playerModal.querySelector("[data-dismiss]"); assert.ok(backdrop, "modal backdrop should be present"); @@ -73,8 +108,10 @@ test("backdrop data-dismiss closes the modal and restores focus", async () => { assert.strictEqual(document.activeElement, trigger); }); -test("Escape key closes the modal and returns focus to the trigger", async () => { - const { window, document, modal, playerModal, trigger } = await setupModal(); +test("Escape key closes the modal and returns focus to the trigger", async (t) => { + const { window, document, modal, playerModal, trigger, cleanup } = + await setupModal(); + t.after(cleanup); const closeButton = playerModal.querySelector("#closeModal"); assert.ok(closeButton, "close button should be present"); @@ -102,3 +139,140 @@ test("Escape key closes the modal and returns focus to the trigger", async () => assert.equal(closeEvents, 1); assert.strictEqual(document.activeElement, trigger); }); + +for (const designSystemEnabled of [false, true]) { + const modeLabel = designSystemEnabled ? "design-system" : "legacy"; + + test( + `[${modeLabel}] video modal sticky navigation responds to scroll direction`, + async (t) => { + const { + window, + document, + modal, + playerModal, + trigger, + cleanup, + } = await setupModal({ designSystemEnabled }); + t.after(cleanup); + + modal.open(null, { triggerElement: trigger }); + await Promise.resolve(); + + const nav = document.getElementById("modalNav"); + assert.ok(nav, "navigation bar should exist"); + const scrollRegion = playerModal.querySelector(".bv-modal__panel"); + assert.ok(scrollRegion, "modal panel should exist"); + + let scrollPosition = 0; + Object.defineProperty(scrollRegion, "scrollTop", { + configurable: true, + get() { + return scrollPosition; + }, + set(value) { + scrollPosition = Number(value) || 0; + } + }); + + scrollRegion.scrollTop = 120; + assert.equal(modal.scrollRegion.scrollTop, 120); + modal.modalNavScrollHandler?.(); + assert.equal(nav.style.transform, "translateY(-100%)"); + + scrollRegion.scrollTop = 60; + assert.equal(modal.scrollRegion.scrollTop, 60); + modal.modalNavScrollHandler?.(); + assert.equal(nav.style.transform, "translateY(0)"); + + scrollRegion.scrollTop = 10; + assert.equal(modal.scrollRegion.scrollTop, 10); + modal.modalNavScrollHandler?.(); + assert.equal(nav.style.transform, "translateY(0)"); + } + ); + + test( + `[${modeLabel}] video modal toggles document scroll locking on open/close`, + async (t) => { + const { document, modal, trigger, cleanup } = await setupModal({ + designSystemEnabled, + }); + t.after(cleanup); + + modal.open(null, { triggerElement: trigger }); + await Promise.resolve(); + + assert.equal( + document.documentElement.classList.contains("modal-open"), + true + ); + assert.equal(document.body.classList.contains("modal-open"), true); + + modal.close(); + await Promise.resolve(); + + assert.equal( + document.documentElement.classList.contains("modal-open"), + false + ); + assert.equal(document.body.classList.contains("modal-open"), false); + assert.strictEqual(document.activeElement, trigger); + } + ); + + test( + `[${modeLabel}] video modal zap dialog updates aria state while toggling`, + async (t) => { + const { + window, + document, + modal, + playerModal, + trigger, + cleanup, + } = await setupModal({ designSystemEnabled }); + t.after(cleanup); + + modal.open(null, { triggerElement: trigger }); + await Promise.resolve(); + + const zapButton = document.getElementById("modalZapBtn"); + const zapDialog = document.getElementById("modalZapDialog"); + const amountInput = document.getElementById("modalZapAmountInput"); + assert.ok(zapButton, "zap trigger should exist"); + assert.ok(zapDialog, "zap dialog should exist"); + assert.ok(amountInput, "zap amount input should exist"); + + modal.setZapVisibility(true); + + assert.equal(zapDialog.hidden, true); + assert.equal(zapDialog.dataset.state, "closed"); + assert.equal(zapButton.getAttribute("aria-expanded"), "false"); + + zapButton.dispatchEvent( + new window.MouseEvent("click", { bubbles: true, cancelable: true }) + ); + + assert.equal(zapDialog.hidden, false); + assert.equal(zapDialog.dataset.state, "open"); + assert.equal(zapDialog.getAttribute("aria-hidden"), "false"); + assert.equal(zapButton.getAttribute("aria-expanded"), "true"); + assert.strictEqual(document.activeElement, amountInput); + + const closeButton = document.getElementById("modalZapCloseBtn"); + assert.ok(closeButton, "zap close button should exist"); + + closeButton.dispatchEvent( + new window.MouseEvent("click", { bubbles: true, cancelable: true }) + ); + + assert.equal(zapDialog.hidden, true); + assert.equal(zapDialog.dataset.state, "closed"); + assert.equal(zapDialog.getAttribute("aria-hidden"), "true"); + assert.equal(zapButton.getAttribute("aria-expanded"), "false"); + + modal.close(); + } + ); +}