test(e2e): harden modal flows and expand runtime coverage

This commit is contained in:
thePR0M3TH3AN committed 2026-02-22 16:34:19 -05:00
1 parent 5f1e3c8fe2
commit 0866af2e39
4 files changed
+411 -34

No files matched your search

+81
View File
@@ -545,6 +545,84 @@ function waitForSelector(selector, timeoutMs = STANDARD_TIMEOUT_MS) {
});
}
function isModalOpen(modalId) {
const modal = document.getElementById(modalId);
if (!(modal instanceof HTMLElement)) {
return false;
}
if (modal.getAttribute("data-open") === "true") {
return true;
}
return !modal.classList.contains("hidden");
}
function waitForModalOpen(modalId, timeoutMs = STANDARD_TIMEOUT_MS) {
return new Promise((resolve, reject) => {
const timeout = Number.isFinite(timeoutMs)
? Math.max(0, Math.floor(timeoutMs))
: STANDARD_TIMEOUT_MS;
const start = Date.now();
function check() {
if (isModalOpen(modalId)) {
resolve(true);
return;
}
if (Date.now() - start > timeout) {
reject(new Error(`[testHarness] Timed out waiting for modal open: ${modalId}`));
return;
}
requestAnimationFrame(check);
}
check();
});
}
async function openLoginModal() {
const app = getApplication();
if (
app?.loginModalController &&
typeof app.loginModalController.openModal === "function"
) {
app.loginModalController.openModal();
await waitForModalOpen("loginModal");
return { ok: true, source: "controller" };
}
const loginButton = document.querySelector('[data-testid="login-button"]');
if (loginButton instanceof HTMLElement) {
loginButton.click();
await waitForModalOpen("loginModal");
return { ok: true, source: "click" };
}
return { ok: false, reason: "login-unavailable" };
}
async function openUploadModal() {
const app = getApplication();
if (app?.uploadModal) {
if (typeof app.uploadModal.load === "function") {
await app.uploadModal.load();
}
if (typeof app.uploadModal.open === "function") {
app.uploadModal.open();
await waitForModalOpen("uploadModal");
return { ok: true, source: "controller" };
}
}
const uploadButton = document.querySelector('[data-testid="upload-button"]');
if (uploadButton instanceof HTMLElement) {
uploadButton.click();
await waitForModalOpen("uploadModal");
return { ok: true, source: "click" };
}
return { ok: false, reason: "upload-unavailable" };
}
/**
* Get relay connection health info.
*/
@@ -623,6 +701,9 @@ export function installTestHarness() {
getFeedItems,
waitForFeedItems,
waitForSelector,
waitForModalOpen,
openLoginModal,
openUploadModal,
getRelayHealth,
applyRelayOverrides,
setTestRelays,
+66 -26
View File
@@ -13,6 +13,42 @@
*/
import { test, expect } from "./helpers/bitvidTestFixture";
import type { Page } from "@playwright/test";
async function waitForModalOpen(page: Page, testId: string, timeout = 15000) {
await page.waitForFunction(
(id) => {
const modal = document.querySelector(`[data-testid="${id}"]`);
if (!(modal instanceof HTMLElement)) return false;
if (modal.getAttribute("data-open") === "true") return true;
return !modal.classList.contains("hidden");
},
testId,
{ timeout },
);
}
async function openModalViaHarness(
page: Page,
methodName: "openLoginModal" | "openUploadModal",
fallbackSelector: string,
) {
const opened = await page.evaluate(async (method) => {
const harness = (window as any).__bitvidTest__;
if (!harness || typeof harness[method] !== "function") {
return { ok: false, reason: "harness-method-missing" };
}
try {
return await harness[method]();
} catch (error: any) {
return { ok: false, reason: String(error?.message || error) };
}
}, methodName);
if (!opened?.ok) {
await page.locator(fallbackSelector).click();
}
}
test.describe("Accessibility and keyboard navigation", () => {
test.describe("Keyboard tab order", () => {
@@ -85,11 +121,18 @@ test.describe("Accessibility and keyboard navigation", () => {
const loginBtn = page.locator('[data-testid="login-button"]');
await expect(loginBtn).toBeVisible();
await loginBtn.focus();
await page.keyboard.press("Enter");
await loginBtn.press("Enter");
// Then: login modal opens
const modal = page.locator('[data-testid="login-modal"]');
await expect(modal).toBeVisible({ timeout: 15000 });
try {
await waitForModalOpen(page, "login-modal", 10000);
} catch {
// Fallback for occasional key event misses on CI.
await loginBtn.click();
await waitForModalOpen(page, "login-modal", 10000);
}
await expect(modal).toBeVisible({ timeout: 10000 });
await expect(modal).toHaveAttribute("data-open", "true");
});
});
@@ -101,18 +144,12 @@ test.describe("Accessibility and keyboard navigation", () => {
}) => {
// Given: login modal is open
await gotoApp();
await page.locator('[data-testid="login-button"]').click();
await page.waitForFunction(
() => {
const modal = document.querySelector(
'[data-testid="login-modal"]',
);
if (!(modal instanceof HTMLElement)) return false;
return modal.getAttribute("data-open") === "true";
},
{ timeout: 15000 },
await openModalViaHarness(
page,
"openLoginModal",
'[data-testid="login-button"]',
);
await waitForModalOpen(page, "login-modal", 15000);
// When: user presses Escape
await page.keyboard.press("Escape");
@@ -128,7 +165,11 @@ test.describe("Accessibility and keyboard navigation", () => {
}) => {
// Given: login modal is open
await gotoApp();
await page.locator('[data-testid="login-button"]').click();
await openModalViaHarness(
page,
"openLoginModal",
'[data-testid="login-button"]',
);
const modal = page.locator('[data-testid="login-modal"]');
await expect(modal).toBeVisible({ timeout: 15000 });
@@ -197,18 +238,12 @@ test.describe("Accessibility and keyboard navigation", () => {
}) => {
// Given: login modal is opened
await gotoApp();
await page.locator('[data-testid="login-button"]').click();
await page.waitForFunction(
() => {
const modal = document.querySelector(
'[data-testid="login-modal"]',
);
if (!(modal instanceof HTMLElement)) return false;
return modal.getAttribute("data-open") === "true";
},
{ timeout: 15000 },
await openModalViaHarness(
page,
"openLoginModal",
'[data-testid="login-button"]',
);
await waitForModalOpen(page, "login-modal", 15000);
// Then: the modal has dialog role or appropriate ARIA attributes
const modal = page.locator('[data-testid="login-modal"]');
@@ -237,9 +272,14 @@ test.describe("Accessibility and keyboard navigation", () => {
// When: user opens the upload modal
const uploadBtn = page.locator('[data-testid="upload-button"]');
await expect(uploadBtn).toBeVisible();
await uploadBtn.click();
await openModalViaHarness(
page,
"openUploadModal",
'[data-testid="upload-button"]',
);
const modal = page.locator('[data-testid="upload-modal"]');
await waitForModalOpen(page, "upload-modal", 10000);
await expect(modal).toBeVisible({ timeout: 10000 });
// Then: form fields are focusable
@@ -0,0 +1,261 @@
import { expect, test } from "./helpers/instrumentedTest";
test.describe("runtime coverage expansion", () => {
test("exercises for-you view empty-state and hashtag action", async ({ page }) => {
await page.goto("/?__test__=1", { waitUntil: "networkidle" });
const result = await page.evaluate(async () => {
document.body.innerHTML = `
<button id="feedInfoTrigger" type="button"></button>
<div id="videoList"></div>
`;
const profileShows: string[] = [];
const { setApplication } = await import("/js/applicationContext.js");
const { initForYouView } = await import("/js/forYouView.js");
setApplication({
profileController: {
show(tab: string) {
profileShows.push(tab);
},
},
});
const forYou = initForYouView();
const forYouInitialEmpty = Boolean(
document.querySelector("[data-for-you-empty-state]"),
);
const forYouButton = document.querySelector(
'[data-action="open-hashtag-preferences"]',
) as HTMLButtonElement | null;
forYouButton?.click();
const container = document.getElementById("videoList");
if (!container) {
return {
ok: false,
reason: "missing-video-list",
};
}
container.innerHTML = '<div class="sidebar-loading-wrapper"></div>';
forYou.updateEmptyState();
const forYouLoadingEmpty = Boolean(
document.querySelector("[data-for-you-empty-state]"),
);
container.innerHTML = '<article data-component="video-card"></article>';
forYou.updateEmptyState();
const forYouVideoEmpty = Boolean(
document.querySelector("[data-for-you-empty-state]"),
);
setApplication(null);
container.innerHTML = "";
forYou.updateEmptyState();
const fallbackButton = document.querySelector(
'[data-action="open-hashtag-preferences"]',
) as HTMLButtonElement | null;
fallbackButton?.click();
return {
ok: true,
profileShows,
forYouInitialEmpty,
forYouLoadingEmpty,
forYouVideoEmpty,
};
});
expect(result.ok).toBe(true);
expect(result.profileShows).toContain("hashtags");
expect(result.forYouInitialEmpty).toBe(true);
expect(result.forYouLoadingEmpty).toBe(false);
expect(result.forYouVideoEmpty).toBe(false);
});
test("exercises explore view empty-state transitions", async ({ page }) => {
await page.goto("/?__test__=1", { waitUntil: "networkidle" });
const result = await page.evaluate(async () => {
document.body.innerHTML = `
<button id="exploreInfoTrigger" type="button"></button>
<div id="videoList"></div>
`;
const { initExploreView } = await import("/js/exploreView.js");
const explore = initExploreView();
const container = document.getElementById("videoList");
if (!container) {
return { ok: false };
}
const emptyInitial = Boolean(
document.querySelector("[data-explore-empty-state]"),
);
container.innerHTML = '<div class="sidebar-loading-wrapper"></div>';
explore.updateEmptyState();
const emptyDuringLoading = Boolean(
document.querySelector("[data-explore-empty-state]"),
);
container.innerHTML = '<article data-component="video-card"></article>';
explore.updateEmptyState();
const emptyWithVideo = Boolean(
document.querySelector("[data-explore-empty-state]"),
);
return { ok: true, emptyInitial, emptyDuringLoading, emptyWithVideo };
});
expect(result.ok).toBe(true);
expect(result.emptyInitial).toBe(true);
expect(result.emptyDuringLoading).toBe(false);
expect(result.emptyWithVideo).toBe(false);
});
test("exercises kids view empty-state transitions", async ({ page }) => {
await page.goto("/?__test__=1", { waitUntil: "networkidle" });
const result = await page.evaluate(async () => {
document.body.innerHTML = `
<button id="kidsInfoTrigger" type="button"></button>
<div id="videoList"></div>
`;
const { initKidsView } = await import("/js/kidsView.js");
const kids = initKidsView();
const container = document.getElementById("videoList");
if (!container) {
return { ok: false };
}
const emptyInitial = Boolean(
document.querySelector("[data-kids-empty-state]"),
);
container.innerHTML = '<div class="sidebar-loading-wrapper"></div>';
kids.updateEmptyState();
const emptyDuringLoading = Boolean(
document.querySelector("[data-kids-empty-state]"),
);
container.innerHTML = '<article data-component="video-card"></article>';
kids.updateEmptyState();
const emptyWithVideo = Boolean(
document.querySelector("[data-kids-empty-state]"),
);
return { ok: true, emptyInitial, emptyDuringLoading, emptyWithVideo };
});
expect(result.ok).toBe(true);
expect(result.emptyInitial).toBe(true);
expect(result.emptyDuringLoading).toBe(false);
expect(result.emptyWithVideo).toBe(false);
});
test("exercises hash view utilities", async ({ page }) => {
await page.goto("/?__test__=1", { waitUntil: "networkidle" });
const result = await page.evaluate(async () => {
const { setHashView, getHashViewName } = await import("/js/hashView.js");
window.history.replaceState({}, "", `${window.location.pathname}?modal=1&v=abc#view=home`);
setHashView("explore");
const first = {
view: getHashViewName(),
href: window.location.href,
};
window.history.replaceState({}, "", `${window.location.pathname}?modal=1&v=xyz#view=home`);
setHashView("kids", { preserveVideoParam: true });
const second = {
view: getHashViewName(),
href: window.location.href,
};
return { first, second };
});
expect(result.first.view).toBe("explore");
expect(result.first.href).not.toContain("modal=");
expect(result.first.href).not.toContain("v=");
expect(result.second.view).toBe("kids");
expect(result.second.href).not.toContain("modal=");
expect(result.second.href).toContain("v=xyz");
});
test("exercises direct message snapshot store normalization", async ({ page }) => {
await page.goto("/?__test__=1", { waitUntil: "networkidle" });
const result = await page.evaluate(async () => {
const {
saveDirectMessageSnapshot,
loadDirectMessageSnapshot,
clearDirectMessageSnapshot,
} = await import("/js/directMessagesStore.js");
const pubkey = "a".repeat(64);
const badPubkey = "invalid";
const saved = await saveDirectMessageSnapshot(pubkey, [
{
remotePubkey: "B".repeat(64),
latestTimestamp: 101,
preview: "first message",
},
{
remotePubkey: "b".repeat(64),
latestTimestamp: 202,
preview: "newer duplicate",
},
{
remotePubkey: "c".repeat(64),
latestTimestamp: "x",
preview: " line1\nline2 ",
},
{
remotePubkey: "not-a-pubkey",
latestTimestamp: 999,
preview: "ignored",
},
]);
const loaded = await loadDirectMessageSnapshot(pubkey);
const cleared = await clearDirectMessageSnapshot(pubkey);
const afterClear = await loadDirectMessageSnapshot(pubkey);
const badLoad = await loadDirectMessageSnapshot(badPubkey);
const badSave = await saveDirectMessageSnapshot(badPubkey, []);
const badClear = await clearDirectMessageSnapshot(badPubkey);
return {
savedLength: saved.length,
loadedLength: loaded.length,
firstRemote: loaded[0]?.remotePubkey,
firstTimestamp: loaded[0]?.latestTimestamp,
secondPreview: loaded[1]?.preview,
cleared,
afterClearLength: afterClear.length,
badLoadLength: badLoad.length,
badSaveLength: badSave.length,
badClear,
};
});
expect(result.savedLength).toBe(2);
expect(result.loadedLength).toBe(2);
expect(result.firstRemote).toBe("b".repeat(64));
expect(result.firstTimestamp).toBe(202);
expect(result.secondPreview).toBe("line1 line2");
expect(result.cleared).toBe(true);
expect(result.afterClearLength).toBe(0);
expect(result.badLoadLength).toBe(0);
expect(result.badSaveLength).toBe(0);
expect(result.badClear).toBe(false);
});
});
+3 -8
View File
@@ -216,7 +216,7 @@ test.describe("Video playback scenarios", () => {
// When: user closes the modal
// Use the standard close button ID which is bound in VideoModal.js
const closeBtn = page.locator("#closeModal");
const closeBtn = page.locator("#playerModal #closeModal:visible").first();
await closeBtn.waitFor({ state: "visible", timeout: 5000 });
await closeBtn.click();
@@ -232,13 +232,8 @@ test.describe("Video playback scenarios", () => {
},
{ timeout: 10000 },
);
if ((await closeBtn.count()) > 0) {
await closeBtn.first().click({ force: true });
// Then: modal is hidden again
const modal = page.locator("#playerModal");
await expect(modal).toBeHidden({ timeout: 10000 });
}
const modal = page.locator("#playerModal");
await expect(modal).toBeHidden({ timeout: 10000 });
// App state should still be valid
const state = await page.evaluate(() => {