Files
bitvid/tests/e2e/view-navigation.spec.ts
T
google-labs-jules[bot]andPR0M3TH3AN c58dde0fb6 test: increase waitForFeedItems timeout to prevent flakiness
Increases the timeout for `waitForFeedItems` from 15s to 30s across multiple E2E test files to prevent test flakiness in slower environments (e.g. Firefox in GitHub Actions).

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-03-01 21:20:31 +00:00

313 lines
9.3 KiB
TypeScript

/**
* View navigation scenarios: sidebar links, hash routing, view switching.
*
* Scenarios covered:
* - SCN-nav-sidebar-recent: Recently Added link navigates to the recent videos view
* - SCN-nav-sidebar-kids: For Kids link navigates to the kids view
* - SCN-nav-sidebar-history: History link navigates to the history view
* - SCN-nav-hash-direct: Direct hash navigation renders the correct view
* - SCN-nav-login-reveals: Login reveals For You, Explore, and Subscriptions links
* - SCN-nav-sidebar-collapse: Sidebar collapse toggle works
* - SCN-nav-view-container: Each view has a video-list container
* - SCN-nav-feed-populates: Seeded videos appear in the most-recent-videos view
*/
import { test, expect } from "./helpers/bitvidTestFixture";
test.describe("View navigation — sidebar links", () => {
test("Recently Added sidebar link navigates to the recent videos view", async ({
page,
gotoApp,
}) => {
// Given: the app is loaded
await gotoApp();
// When: user clicks the Recently Added link
const recentLink = page.locator('a[href="#view=most-recent-videos"]');
await expect(recentLink).toBeAttached();
await recentLink.click();
// Then: the URL hash should reflect the view
await page.waitForFunction(
() => window.location.hash.includes("view=most-recent-videos"),
{ timeout: 5000 },
);
// And: the video list container should be present
const videoList = page.locator('[data-testid="video-list"]');
await expect(videoList).toBeAttached({ timeout: 10000 });
});
test("For Kids sidebar link navigates to the kids view", async ({
page,
gotoApp,
}) => {
// Given: the app is loaded
await gotoApp();
// When: user clicks the For Kids link
const kidsLink = page.locator('a[href="#view=kids"]');
await expect(kidsLink).toBeAttached();
await kidsLink.click();
// Then: the URL hash should reflect the kids view
await page.waitForFunction(
() => window.location.hash.includes("view=kids"),
{ timeout: 5000 },
);
});
test("History sidebar link navigates to the history view", async ({
page,
gotoApp,
}) => {
// Given: the app is loaded
await gotoApp();
// When: user clicks the History link
const historyLink = page.locator('a[href="#view=history"]');
await expect(historyLink).toBeAttached();
await historyLink.click();
// Then: the URL hash should reflect the history view
await page.waitForFunction(
() => window.location.hash.includes("view=history"),
{ timeout: 5000 },
);
});
});
test.describe("View navigation — login-gated links", () => {
test("For You, Explore, and Subscriptions links are hidden before login", async ({
page,
gotoApp,
}) => {
// Given: the app is loaded without login
await gotoApp();
// Then: login-gated links should be hidden
const forYouLink = page.locator("#forYouLink");
const exploreLink = page.locator("#exploreLink");
const subscriptionsLink = page.locator("#subscriptionsLink");
await expect(forYouLink).toHaveClass(/hidden/);
await expect(exploreLink).toHaveClass(/hidden/);
await expect(subscriptionsLink).toHaveClass(/hidden/);
});
test("login reveals For You, Explore, and Subscriptions sidebar links", async ({
page,
gotoApp,
loginAs,
}) => {
// Given: the app is loaded
await gotoApp();
// When: user logs in
await loginAs(page);
// Then: the login-gated links should become visible
// Allow time for the UI to update after login
const forYouLink = page.locator("#forYouLink");
const exploreLink = page.locator("#exploreLink");
const subscriptionsLink = page.locator("#subscriptionsLink");
await expect(forYouLink).not.toHaveClass(/hidden/, { timeout: 10000 });
await expect(exploreLink).not.toHaveClass(/hidden/, { timeout: 10000 });
await expect(subscriptionsLink).not.toHaveClass(/hidden/, { timeout: 10000 });
});
test("For You link navigates to the for-you view after login", async ({
page,
gotoApp,
loginAs,
}) => {
// Given: user is logged in
await gotoApp();
await loginAs(page);
const forYouLink = page.locator("#forYouLink");
await expect(forYouLink).not.toHaveClass(/hidden/, { timeout: 10000 });
// When: user clicks For You
await forYouLink.click();
// Then: hash should reflect the for-you view
await page.waitForFunction(
() => window.location.hash.includes("view=for-you"),
{ timeout: 5000 },
);
});
});
test.describe("View navigation — direct hash routing", () => {
test("navigating to #view=kids directly loads the kids view", async ({
page,
gotoApp,
}) => {
// Given: the app is loaded at a specific hash
await gotoApp("/#view=kids");
// Then: the hash should be set
const hash = await page.evaluate(() => window.location.hash);
expect(hash).toContain("view=kids");
});
test("navigating to #view=most-recent-videos shows the feed", async ({
page,
gotoApp,
loginAs,
seedEvent,
}) => {
// Given: a video exists
await seedEvent({
title: "Navigation Feed Video",
url: "https://example.com/nav-feed.mp4",
dTag: "nav-feed-001",
});
await gotoApp("/#view=most-recent-videos");
await loginAs(page);
// Then: the feed should populate with the seeded video
const items = await page.evaluate(() => {
return (window as any).__bitvidTest__.waitForFeedItems(1, 30000);
});
expect(items.length).toBeGreaterThanOrEqual(1);
});
});
test.describe("View navigation — sidebar collapse", () => {
test("sidebar collapse toggle changes sidebar state", async ({
page,
gotoApp,
}) => {
// Given: the app is loaded with sidebar collapsed (default)
await gotoApp();
const sidebar = page.locator("#sidebar");
const toggle = page.locator("#sidebarCollapseToggle");
await expect(sidebar).toBeAttached();
await expect(toggle).toBeAttached();
// Record initial state
const initialState = await sidebar.getAttribute("data-state");
// When: user clicks the collapse toggle
await toggle.click();
// Then: the sidebar state should change
await page.waitForFunction(
(initial) => {
const sidebar = document.querySelector("#sidebar");
return sidebar?.getAttribute("data-state") !== initial;
},
initialState,
{ timeout: 5000 },
);
const newState = await sidebar.getAttribute("data-state");
expect(newState).not.toBe(initialState);
});
test("sidebar toggle updates aria-expanded attribute", async ({
page,
gotoApp,
}) => {
// Given: the app is loaded
await gotoApp();
const toggle = page.locator("#sidebarCollapseToggle");
const initialExpanded = await toggle.getAttribute("aria-expanded");
// When: user clicks the toggle
await toggle.click();
// Then: aria-expanded should flip
const expected = initialExpanded === "true" ? "false" : "true";
await expect(toggle).toHaveAttribute("aria-expanded", expected, {
timeout: 5000,
});
});
});
test.describe("View navigation — feed with seeded content", () => {
test("seeded videos appear in the feed after navigation", async ({
page,
gotoApp,
loginAs,
seedEvent,
}) => {
// Given: multiple videos are seeded
await seedEvent({
title: "Nav Video One",
url: "https://example.com/nav1.mp4",
dTag: "nav-multi-001",
});
await seedEvent({
title: "Nav Video Two",
url: "https://example.com/nav2.mp4",
dTag: "nav-multi-002",
});
await gotoApp();
await loginAs(page);
// Then: both videos should appear in the feed
const items = await page.evaluate(() => {
return (window as any).__bitvidTest__.waitForFeedItems(2, 30000);
});
expect(items.length).toBeGreaterThanOrEqual(2);
const titles = items.map((i: any) => i.title);
expect(titles).toContain("Nav Video One");
expect(titles).toContain("Nav Video Two");
});
test("switching views and returning preserves app state", async ({
page,
gotoApp,
loginAs,
seedEvent,
}) => {
// Given: user is logged in and viewing the feed
await seedEvent({
title: "Persistent Video",
url: "https://example.com/persist.mp4",
dTag: "nav-persist-001",
});
await gotoApp();
await loginAs(page);
await page.evaluate(() => {
return (window as any).__bitvidTest__.waitForFeedItems(1, 30000);
});
// When: user navigates to kids view and back
await page.locator('a[href="#view=kids"]').click();
await page.waitForFunction(
() => window.location.hash.includes("view=kids"),
{ timeout: 5000 },
);
await page.locator('a[href="#view=most-recent-videos"]').click();
await page.waitForFunction(
() => window.location.hash.includes("view=most-recent-videos"),
{ timeout: 5000 },
);
// Then: app state should remain valid (login preserved across view switches)
const state = await page.evaluate(() => {
return (window as any).__bitvidTest__.getAppState();
});
expect(state.isLoggedIn).toBe(true);
expect(state.activePubkey).toBeTruthy();
// And: the video list container should be present in the returned view
const videoList = page.locator('[data-testid="video-list"]');
await expect(videoList).toBeAttached({ timeout: 10000 });
});
});