import { expect, test, type Page } from "@playwright/test"; import { CHANNEL } from "./fixtures/data"; import { installRoutes, openFilters } from "./helpers"; const STORAGE_KEY = "ytdlp-tb:export-filters"; // Smoke-test the post-refactor search UI: // - "slow / medium / fast" cost badges are gone from each layer's scope. // - "Reset layers" sits next to Search and reverts the builder to one empty // transcripts leaf. // - The Profile row renders *above* the QueryBuilder, regardless of the // channel count. // - Share button lives inside the Profile row. // - The dirty-marker uses a `•` glyph rather than the word "(unsaved)". // - A "Revert" button shows up only when the active profile is dirty, and // restores the saved snapshot. // - "Reset everything" wipes both layers and filters (covered with a // multi-channel route override so the gated reset row renders). test.describe("search UI — profile row + reset refinements", () => { test.beforeEach(async ({ page }) => { await installRoutes(page); }); test("scope dropdown no longer renders slow/medium/fast cost badges", async ({ page, }) => { await page.goto("/"); // Wait for the builder to render (the scope . // After the change, no such literal text should be in the DOM. await expect(page.getByText(/^slow$/i)).toHaveCount(0); await expect(page.getByText(/^medium$/i)).toHaveCount(0); await expect(page.getByText(/^fast$/i)).toHaveCount(0); }); test("Reset layers reverts a multi-layer tree to one empty leaf", async ({ page, }) => { // Seed a 2-leaf tree via the canonical `?qt=` param. const tree = { k: "g", o: "AND", c: [ { k: "l", q: "alpha", s: "transcripts" }, { k: "l", q: "beta", s: "transcripts" }, ], }; await page.goto(`/?qt=${encodeURIComponent(JSON.stringify(tree))}`); // The builder should be in full (non-compact) mode with 2 leaves. const leafInputs = page.locator('[data-testid^="leaf-query-"]'); await expect(leafInputs).toHaveCount(2); const reset = page.getByTestId("reset-layers"); await expect(reset).toBeEnabled(); await reset.click(); // After reset: one empty leaf, button disabled. await expect(leafInputs).toHaveCount(1); await expect(leafInputs.first()).toHaveValue(""); await expect(reset).toBeDisabled(); }); test("Profile row lives in the filters panel, even with one channel", async ({ page, }) => { await page.goto("/"); // A profile IS a saved filter+query set, so it sits inside the filters // panel rather than as a third row above the search box. Its controls are // reachable wherever the panel is placed (inline from xl, a sheet below). await openFilters(page); const panel = page.getByTestId("filters-panel"); const profilesRow = panel.getByTestId("profiles-row"); await expect(profilesRow).toBeVisible(); await expect(page.getByTestId("query-builder")).toBeVisible(); }); test("Share button lives inside the Profile row", async ({ page }) => { await page.goto("/"); await openFilters(page); const profilesRow = page.getByTestId("profiles-row"); const shareInRow = profilesRow.getByRole("button", { name: "Share current search", }); await expect(shareInRow).toBeVisible(); }); // A profile + URL share-v1 overlay combo that makes committed diverge from // the active profile snapshot without going through commitSearch (which // would otherwise clear `activeProfileName`). We seed `query` on the // profile and pass the *same* `qt=` in the URL so the snapshot's `query` // field matches what `committedSnapshot.query` will be — that way the // only diverging field is the filter we want to test (nol). const ALPHA_TREE_QS = encodeURIComponent( JSON.stringify({ k: "g", o: "AND", c: [{ k: "l", q: "alpha", s: "transcripts" }], }), ); const PROFILE_QUERY = JSON.stringify({ k: "g", o: "AND", c: [{ k: "l", q: "alpha", s: "transcripts" }], }); async function seedDirtyProfile(page: Page) { await page.goto("/"); await page.evaluate( ({ key, value }) => { window.localStorage.setItem(key, value); }, { key: STORAGE_KEY, value: JSON.stringify({ v: 1, working: { channels: { included: [], excluded: [] }, nol: true, query: PROFILE_QUERY, }, profiles: { p1: { channels: { included: [], excluded: [] }, nol: true, query: PROFILE_QUERY, }, }, activeProfileName: "p1", }), }, ); // share-v1 overlay: Livestreams selected (nol=false) so committed // diverges from the profile (nol=true). Build the QS manually because // URLSearchParams.toString re-encodes the already-encoded `qt`. const qs = [ `qt=${ALPHA_TREE_QS}`, "fv=1", `fc=${encodeURIComponent(CHANNEL)}`, "ft=v", "ft=l", "fa=a", "fa=r", "fav=a", "fav=d", ].join("&"); await page.goto(`/?${qs}`); } test("dirty marker is a • glyph, not the word '(unsaved)'", async ({ page, }) => { await seedDirtyProfile(page); // The profile is selected and the dirty dot is visible. await expect(page.getByTestId("profile-select")).toHaveValue("p1"); const dot = page.getByTestId("profile-dirty-dot"); await expect(dot).toBeVisible(); await expect(dot).toHaveText("•"); // "(unsaved)" still appears as the placeholder