mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-01 18:32:27 +00:00
Merge pull request #449 from PR0M3TH3AN/codex/add-integration-tests-for-video-modal
Add modal accessibility integration coverage
This commit is contained in:
4 files changed
+694
-88
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
"<!DOCTYPE html><html><body><button id=\"trigger\">Upload</button><div id=\"modalContainer\"></div></body></html>",
|
||||
),
|
||||
);
|
||||
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(
|
||||
"<!DOCTYPE html><html><body><button id=\"trigger\">Upload</button><div id=\"modalContainer\"></div></body></html>",
|
||||
),
|
||||
);
|
||||
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(
|
||||
"<!DOCTYPE html><html><body><button id=\"trigger\">Edit</button><div id=\"modalContainer\"></div></body></html>",
|
||||
),
|
||||
);
|
||||
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(
|
||||
"<!DOCTYPE html><html><body><button id=\"trigger\">Upload</button><div id=\"modalContainer\"></div></body></html>",
|
||||
),
|
||||
);
|
||||
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(
|
||||
"<!DOCTYPE html><html><body><div id=\"modalContainer\"></div></body></html>",
|
||||
),
|
||||
);
|
||||
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(
|
||||
"<!DOCTYPE html><html><body><button id=\"trigger\">Edit</button><div id=\"modalContainer\"></div></body></html>",
|
||||
),
|
||||
);
|
||||
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(
|
||||
"<!DOCTYPE html><html><body><button id=\"trigger\">Edit</button><div id=\"modalContainer\"></div></body></html>",
|
||||
),
|
||||
);
|
||||
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(
|
||||
"<!DOCTYPE html><html><body><div id=\"modalContainer\"></div></body></html>",
|
||||
),
|
||||
);
|
||||
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;
|
||||
|
||||
@@ -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(
|
||||
'<!DOCTYPE html><html><body><div id="modalMount"></div></body></html>',
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
`<!DOCTYPE html><html><body><button id="trigger" type="button">Open modal</button><div id="modalContainer">${markup}</div></body></html>`,
|
||||
{ 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();
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user