Files
bitvid/tests/e2e/search-and-filter.spec.ts
T
google-labs-jules[bot]andPR0M3TH3AN d0cab85b25 test: increase playwright e2e timeouts to reduce flakiness
Double default timeouts and extend the explicit waitForFeedItems calls across the e2e test suite to accommodate slower CI runners. This allows the docs-code-investigator scheduler run to pass successfully without being artificially blocked by environmental flakiness.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-03-02 20:43:39 +00:00

212 lines
6.6 KiB
TypeScript

/**
* Search input behavior, feed filtering, and empty state tests.
*
* Scenarios covered:
* - SCN-search-visible: Search input is present and accessible on page load
* - SCN-search-type: Typing in search input updates its value
* - SCN-search-filter-match: Search text filters feed to matching videos
* - SCN-search-filter-nomatch: Non-matching search text yields empty results
* - SCN-search-clear: Clearing search restores full feed
* - SCN-search-special-chars: Special characters don't crash the app
*/
import { test, expect } from "./helpers/bitvidTestFixture";
test.describe("Search and filtering", () => {
test.describe("Search input accessibility", () => {
test("search input is visible and focusable on page load", async ({
page,
gotoApp,
}) => {
// Given: the app is loaded
await gotoApp();
// Then: search input is visible and can receive focus
const searchInput = page.locator('[data-testid="search-input"]');
await expect(searchInput).toBeVisible();
await searchInput.focus();
const isFocused = await page.evaluate(() => {
const el = document.querySelector('[data-testid="search-input"]');
return document.activeElement === el;
});
expect(isFocused).toBe(true);
});
test("search input accepts text input", async ({ page, gotoApp }) => {
// Given: the app is loaded
await gotoApp();
// When: user types into the search input
const searchInput = page.locator('[data-testid="search-input"]');
await searchInput.fill("test query");
// Then: the input value reflects the typed text
await expect(searchInput).toHaveValue("test query");
});
});
test.describe("Feed filtering behavior", () => {
test("search filters feed to show only matching videos", async ({
page,
gotoApp,
loginAs,
seedEvent,
}) => {
// Given: multiple videos with distinct titles
await seedEvent({
title: "Cooking Pasta Tutorial",
url: "https://example.com/pasta.mp4",
dTag: "search-pasta-001",
});
await seedEvent({
title: "Guitar Lessons Beginner",
url: "https://example.com/guitar.mp4",
dTag: "search-guitar-001",
});
await seedEvent({
title: "Cooking Sushi Masterclass",
url: "https://example.com/sushi.mp4",
dTag: "search-sushi-001",
});
await gotoApp();
await loginAs(page);
// Wait for all videos to load
await page.evaluate(() => {
return (window as any).__bitvidTest__.waitForFeedItems(3, 60000);
});
// When: user searches for "Cooking"
const searchInput = page.locator('[data-testid="search-input"]');
await searchInput.fill("Cooking");
// Allow debounce/filter to apply
await page.waitForTimeout(500);
// Then: only cooking videos should be visible
const visibleCards = page.locator("[data-video-card]:visible");
const count = await visibleCards.count();
// If filtering is implemented, we should see fewer results
// If not filtered (search might be server-side or not yet implemented),
// at minimum we verify the search input state is preserved
await expect(searchInput).toHaveValue("Cooking");
// Check that the app didn't crash — no page errors
const state = await page.evaluate(() => {
return (window as any).__bitvidTest__.getAppState();
});
expect(state).toBeTruthy();
});
test("search with no matching results shows appropriate state", async ({
page,
gotoApp,
loginAs,
seedEvent,
}) => {
// Given: a video that won't match our search
await seedEvent({
title: "Regular Video Content",
url: "https://example.com/regular.mp4",
dTag: "search-nomatch-001",
});
await gotoApp();
await loginAs(page);
await page.evaluate(() => {
return (window as any).__bitvidTest__.waitForFeedItems(1, 60000);
});
// When: user searches for something that doesn't match
const searchInput = page.locator('[data-testid="search-input"]');
await searchInput.fill("xyznonexistent99999");
// Allow search to process
await page.waitForTimeout(500);
// Then: app should not crash and search input maintains value
await expect(searchInput).toHaveValue("xyznonexistent99999");
const state = await page.evaluate(() => {
return (window as any).__bitvidTest__.getAppState();
});
expect(state).toBeTruthy();
});
test("clearing search input restores feed state", async ({
page,
gotoApp,
loginAs,
seedEvent,
}) => {
// Given: seeded videos and a search has been performed
await seedEvent({
title: "Clearable Search Video",
url: "https://example.com/clear.mp4",
dTag: "search-clear-001",
});
await gotoApp();
await loginAs(page);
await page.evaluate(() => {
return (window as any).__bitvidTest__.waitForFeedItems(1, 60000);
});
const searchInput = page.locator('[data-testid="search-input"]');
await searchInput.fill("something");
await page.waitForTimeout(300);
// When: user clears the search
await searchInput.clear();
await page.waitForTimeout(500);
// Then: input is empty and app state is valid
await expect(searchInput).toHaveValue("");
const state = await page.evaluate(() => {
return (window as any).__bitvidTest__.getAppState();
});
expect(state).toBeTruthy();
});
test("search input handles special characters without errors", async ({
page,
gotoApp,
startDiagnostics,
}) => {
// Given: app is loaded with diagnostics
await gotoApp();
const diag = await startDiagnostics(page);
// When: user types special characters that could trigger regex/XSS issues
const searchInput = page.locator('[data-testid="search-input"]');
const specialInputs = [
'<script>alert("xss")</script>',
"test[regex](pattern)",
"video & music | art",
'quote"test\'value',
"unicode: 🎥📺🔍",
];
for (const input of specialInputs) {
await searchInput.fill(input);
await page.waitForTimeout(200);
await expect(searchInput).toHaveValue(input);
}
// Then: no page errors occurred
const results = await diag.stop();
const criticalErrors = results.pageErrors.filter(
(e) => !e.includes("net::") && !e.includes("favicon"),
);
expect(criticalErrors).toEqual([]);
});
});
});