import { test, expect } from "./coverage-fixtures.js"; const MOCK_MODELS_RESPONSE = { models: [ { name: "llama-model", description: "A llama model", backend: "llama-cpp", installed: false, tags: ["chat"], // The listing carries only the declaration flag. Describing variants // costs the server a network probe each, so the description lives // behind /api/models/variants/:id and is fetched on demand. has_variants: true, }, { name: "whisper-model", description: "A whisper model", backend: "whisper", installed: true, tags: ["transcript"], }, { name: "stablediffusion-model", description: "An image model", backend: "stablediffusion", installed: false, tags: ["sd"], }, { name: "unknown-model", description: "No backend", backend: "", installed: false, tags: [], }, ], allBackends: ["llama-cpp", "stablediffusion", "whisper"], allTags: ["chat", "sd", "transcript"], availableModels: 4, installedModels: 1, totalPages: 1, currentPage: 1, }; const MOCK_GPU_RESOURCES_RESPONSE = { type: "gpu", available: true, gpus: [ { index: 0, name: "Mock GPU", vendor: "nvidia", total_vram: 12 * 1024 * 1024 * 1024, used_vram: 2 * 1024 * 1024 * 1024, free_vram: 10 * 1024 * 1024 * 1024, usage_percent: 16.7, }, ], aggregate: { total_memory: 12 * 1024 * 1024 * 1024, used_memory: 2 * 1024 * 1024 * 1024, free_memory: 10 * 1024 * 1024 * 1024, usage_percent: 16.7, gpu_count: 1, }, }; const MOCK_ESTIMATES = { "llama-model": { sizeBytes: 4 * 1024 * 1024 * 1024, sizeDisplay: "4.00 GB", estimates: { 8192: { vramBytes: 8 * 1024 * 1024 * 1024, vramDisplay: "8.00 GB", }, }, }, "whisper-model": { sizeBytes: 1 * 1024 * 1024 * 1024, sizeDisplay: "1.00 GB", estimates: { 8192: { vramBytes: 2 * 1024 * 1024 * 1024, vramDisplay: "2.00 GB", }, }, }, "stablediffusion-model": { sizeBytes: 8 * 1024 * 1024 * 1024, sizeDisplay: "8.00 GB", estimates: { 8192: { vramBytes: 16 * 1024 * 1024 * 1024, vramDisplay: "16.00 GB", }, }, }, }; // The gallery is a rail plus a pane, not a table. These three helpers are the // whole of that migration for the specs below: an entry is addressed by the // model it carries, and the detail lives in the pane rather than in a cell // spanning the row. const PANE = '[data-testid="discover-pane"]'; const railItems = (page) => page.locator('[data-testid="discover-rail-item"]'); const railItem = (page, name) => page.locator(`[data-entity="${name}"]`); // The use-case chips live in a popover now; opening it is idempotent so tests // can call this without tracking whether it is already up. const openUseCases = async (page) => { const trigger = page.locator(".models-filters__usecase-trigger"); if ((await page.locator(".filter-btn").count()) === 0) await trigger.click(); await expect(page.locator(".filter-btn").first()).toBeVisible(); }; // Rendered means the rail has entries. The old gate waited on a column header. const railReady = (page) => expect(railItems(page).first()).toBeVisible({ timeout: 10_000 }); test.describe("Models Gallery - Backend Features", () => { test.beforeEach(async ({ page }) => { await page.route("**/api/models*", (route) => { route.fulfill({ contentType: "application/json", body: JSON.stringify(MOCK_MODELS_RESPONSE), }); }); await page.goto("/app/models"); // Wait for the table to render await railReady(page); }); test("selecting a model names its backend in the pane", async ({ page }) => { await railItem(page, "llama-model").click(); await expect( page.locator(PANE).locator(".badge", { hasText: "llama-cpp" }).first(), ).toBeVisible(); await railItem(page, "whisper-model").click(); await expect( page.locator(PANE).locator(".badge", { hasText: /^whisper$/ }).first(), ).toBeVisible(); }); test("backend dropdown is visible", async ({ page }) => { await expect( page.locator("button", { hasText: "All Backends" }), ).toBeVisible(); }); test("clicking backend dropdown opens searchable panel", async ({ page }) => { await page.locator("button", { hasText: "All Backends" }).click(); await expect( page.locator('input[placeholder="Search backends..."]'), ).toBeVisible(); }); test("typing in search filters dropdown options", async ({ page }) => { await page.locator("button", { hasText: "All Backends" }).click(); const searchInput = page.locator('input[placeholder="Search backends..."]'); await searchInput.fill("llama"); // llama-cpp option should be visible, whisper should not const dropdown = page .locator('input[placeholder="Search backends..."]') .locator("..") .locator(".."); await expect(dropdown.locator("text=llama-cpp")).toBeVisible(); await expect(dropdown.locator("text=whisper")).not.toBeVisible(); }); test("selecting a backend updates the dropdown label", async ({ page }) => { await page.locator("button", { hasText: "All Backends" }).click(); // Click the llama-cpp option within the dropdown (not the table badge) const dropdown = page .locator('input[placeholder="Search backends..."]') .locator("..") .locator(".."); await dropdown.locator("text=llama-cpp").click(); // Scoped to the select: the rail names a backend on its own entries when // no size estimate has arrived, so an unscoped `button span` matches those // too and the assertion stops being about the dropdown. await expect( page.locator(".models-filters__backend button span", { hasText: "llama-cpp" }), ).toBeVisible(); }); test("expanded row shows backend in detail", async ({ page }) => { // Click the first model row to expand it await railItem(page, "llama-model").click(); // The detail view should show Backend label and value const detail = page.locator(PANE); await expect(detail.locator("text=Backend")).toBeVisible(); // The Backend DetailRow renders before the Variants section, which lists a // per-variant backend badge of its own, so scope to the first match. await expect(detail.locator("text=llama-cpp").first()).toBeVisible(); }); }); const BACKEND_USECASES_MOCK = { "llama-cpp": ["chat", "embeddings", "vision", "token_classify"], whisper: ["transcript"], stablediffusion: ["image"], }; const EMPTY_FILTERED_RESPONSE = { ...MOCK_MODELS_RESPONSE, models: [], availableModels: 0, totalPages: 1, currentPage: 1, }; test.describe("Models Gallery - Multi-select Filters", () => { test.beforeEach(async ({ page }) => { await page.route("**/api/models*", (route) => { route.fulfill({ contentType: "application/json", body: JSON.stringify(MOCK_MODELS_RESPONSE), }); }); await page.route("**/api/backends/usecases", (route) => { route.fulfill({ contentType: "application/json", body: JSON.stringify(BACKEND_USECASES_MOCK), }); }); await page.goto("/app/models"); await railReady(page); }); test("multi-select toggle: click Chat, TTS, then Chat again", async ({ page, }) => { await openUseCases(page); const chatBtn = page.locator(".filter-btn", { hasText: "Chat" }); const ttsBtn = page.locator(".filter-btn", { hasText: "TTS" }); await chatBtn.click(); await expect(chatBtn).toHaveClass(/active/); await ttsBtn.click(); await expect(chatBtn).toHaveClass(/active/); await expect(ttsBtn).toHaveClass(/active/); // Click Chat again to deselect it await chatBtn.click(); await expect(chatBtn).not.toHaveClass(/active/); await expect(ttsBtn).toHaveClass(/active/); }); test('"All" clears selection', async ({ page }) => { await openUseCases(page); const chatBtn = page.locator(".filter-btn", { hasText: "Chat" }); const allBtn = page.locator(".filter-btn", { hasText: "All" }); await chatBtn.click(); await expect(chatBtn).toHaveClass(/active/); await allBtn.click(); await expect(allBtn).toHaveClass(/active/); await expect(chatBtn).not.toHaveClass(/active/); }); test("query param sent correctly with multiple filters", async ({ page }) => { await openUseCases(page); const chatBtn = page.locator(".filter-btn", { hasText: "Chat" }); const ttsBtn = page.locator(".filter-btn", { hasText: "TTS" }); // Click Chat and wait for its request to settle await chatBtn.click(); await page.waitForResponse((resp) => resp.url().includes("/api/models")); // Now click TTS and capture the resulting request const [request] = await Promise.all([ page.waitForRequest((req) => { if (!req.url().includes("/api/models")) return false; const u = new URL(req.url()); const tag = u.searchParams.get("tag"); return tag && tag.split(",").length >= 2; }), ttsBtn.click(), ]); const url = new URL(request.url()); const tags = url.searchParams.get("tag").split(",").sort(); expect(tags).toEqual(["chat", "tts"]); }); test("backend greys out unavailable filters", async ({ page }) => { await openUseCases(page); // Select llama-cpp backend via dropdown await page.locator("button", { hasText: "All Backends" }).click(); const dropdown = page .locator('input[placeholder="Search backends..."]') .locator("..") .locator(".."); await dropdown.locator("text=llama-cpp").click(); // Wait for filter state to update const ttsBtn = page.locator(".filter-btn", { hasText: "TTS" }); const sttBtn = page.locator(".filter-btn", { hasText: "STT" }); const imageBtn = page.locator(".filter-btn", { hasText: "Image" }); // TTS, STT, Image should be disabled for llama-cpp await expect(ttsBtn).toBeDisabled(); await expect(sttBtn).toBeDisabled(); await expect(imageBtn).toBeDisabled(); // Chat, Embeddings, Vision, NER should remain enabled const chatBtn = page.locator(".filter-btn", { hasText: "Chat" }); const embBtn = page.locator(".filter-btn", { hasText: "Embeddings" }); const visBtn = page.locator(".filter-btn", { hasText: "Vision" }); const nerBtn = page.locator(".filter-btn", { hasText: "NER" }); await expect(chatBtn).toBeEnabled(); await expect(embBtn).toBeEnabled(); await expect(visBtn).toBeEnabled(); await expect(nerBtn).toBeEnabled(); }); test("backend clears incompatible filters", async ({ page }) => { await openUseCases(page); // Select TTS filter first const ttsBtn = page.locator(".filter-btn", { hasText: "TTS" }); await ttsBtn.click(); await expect(ttsBtn).toHaveClass(/active/); // Now select llama-cpp backend (which doesn't support TTS) await page.locator("button", { hasText: "All Backends" }).click(); const dropdown = page .locator('input[placeholder="Search backends..."]') .locator("..") .locator(".."); await dropdown.locator("text=llama-cpp").click(); // TTS should be auto-removed from selection await expect(ttsBtn).not.toHaveClass(/active/); }); }); test.describe("Models Gallery - Fits In GPU Filter", () => { test.beforeEach(async ({ page }) => { await page.route("**/api/models*", (route) => { route.fulfill({ contentType: "application/json", body: JSON.stringify(MOCK_MODELS_RESPONSE), }); }); await page.route("**/api/resources", (route) => { route.fulfill({ contentType: "application/json", body: JSON.stringify(MOCK_GPU_RESOURCES_RESPONSE), }); }); await page.route("**/api/models/estimate/*", (route) => { const url = new URL(route.request().url()); const id = decodeURIComponent(url.pathname.split("/").pop() || ""); route.fulfill({ contentType: "application/json", body: JSON.stringify(MOCK_ESTIMATES[id] || {}), }); }); await page.goto("/app/models"); await railReady(page); }); test("fits toggle is visible when GPU resources are available", async ({ page, }) => { await expect(page.getByText("Fits in GPU")).toBeVisible(); }); test("enabling fits filter hides models that exceed available VRAM", async ({ page, }) => { await expect( railItem(page, "stablediffusion-model"), ).toBeVisible(); // The shared visually hides its native input (opacity:0;w:0;h:0), // so .check() can't interact with it directly — click the visible track. await page .locator("label.filter-bar-group__toggle", { hasText: "Fits in GPU" }) .locator(".toggle__track") .click(); await expect( railItem(page, "stablediffusion-model"), ).toHaveCount(0); await expect(railItem(page, "llama-model")).toBeVisible(); // Unknown estimate stays visible until an explicit non-fit verdict exists. await expect( railItem(page, "unknown-model"), ).toBeVisible(); }); test("fits filter state persists after reload", async ({ page }) => { await page .locator("label.filter-bar-group__toggle", { hasText: "Fits in GPU" }) .locator(".toggle__track") .click(); await page.reload(); await expect(page.getByLabel("Fits in GPU")).toBeChecked(); }); }); test.describe("Models Gallery - Empty State", () => { test.beforeEach(async ({ page }) => { await page.route("**/api/models*", (route) => { const url = new URL(route.request().url()); const tag = url.searchParams.get("tag"); const body = tag === "chat" ? EMPTY_FILTERED_RESPONSE : MOCK_MODELS_RESPONSE; route.fulfill({ contentType: "application/json", body: JSON.stringify(body), }); }); await page.goto("/app/models"); await railReady(page); }); test("shows empty state for filtered-out results and clear filters restores the gallery", async ({ page, }) => { await openUseCases(page); const chatBtn = page.locator(".filter-btn", { hasText: "Chat" }); const allBtn = page.locator(".filter-btn", { hasText: "All" }); await chatBtn.click(); await expect(page.locator(".empty-state-title")).toHaveText( "No models found", ); await expect(page.locator(".empty-state-text")).toHaveText( "No models match your current search or filters.", ); const clearBtn = page.getByRole("button", { name: "Clear filters" }); await expect(clearBtn).toBeVisible(); await expect(railItem(page, "llama-model")).toHaveCount(0); await clearBtn.click(); await expect(allBtn).toHaveClass(/active/); await expect(chatBtn).not.toHaveClass(/active/); await expect(page.locator(".empty-state")).toHaveCount(0); await expect(railItem(page, "llama-model")).toBeVisible(); }); }); // The variant description the companion endpoint returns for llama-model. // memory_bytes is omitempty server-side, so the mlx variant deliberately // carries no key at all: the UI must render that as unknown, never 0 B. // // quantization and features are omitempty for the same reason. The mlx build // carries neither, standing in for a backend served from a directory of // weights whose name declares no format: those cells must degrade to a stated // "unknown", never to a blank or an "undefined". const MOCK_VARIANTS_RESPONSE = { variants: [ { model: "llama-model", backend: "llama-cpp", memory_bytes: 4 * 1024 * 1024 * 1024, fits: true, is_base: true, quantization: "Q4_K_M", }, { model: "llama-model-q8", backend: "llama-cpp", memory_bytes: 8 * 1024 * 1024 * 1024, fits: true, is_base: false, quantization: "Q8_0", features: ["dflash"], }, { model: "llama-model-mlx", backend: "mlx", fits: true, is_base: false, }, { model: "llama-model-f16", backend: "llama-cpp", memory_bytes: 40 * 1024 * 1024 * 1024, fits: false, is_base: false, quantization: "F16", }, ], auto_selected: "llama-model-q8", }; test.describe("Models Gallery - Variant picker", () => { // installUrls records every install request so a test can assert both the // presence and the absence of the ?variant= parameter. let installUrls; // variantUrls records every companion-endpoint request. It is what proves // the description is fetched lazily and cached, rather than being paid for // by every row on page load. let variantUrls; // Held requests let a test observe the in-flight state rather than racing it. let releaseVariants; test.beforeEach(async ({ page }) => { installUrls = []; variantUrls = []; releaseVariants = null; await page.route("**/api/models*", (route) => { route.fulfill({ contentType: "application/json", body: JSON.stringify(MOCK_MODELS_RESPONSE), }); }); await page.route("**/api/models/install/**", (route) => { installUrls.push(route.request().url()); return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ jobID: "variant-install" }), }); }); await page.route("**/api/models/variants/**", async (route) => { variantUrls.push(route.request().url()); if (releaseVariants) await releaseVariants; return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(MOCK_VARIANTS_RESPONSE), }); }); await page.goto("/app/models"); await railReady(page); }); const variantRow = (page) => railItem(page, "llama-model"); const plainRow = (page) => railItem(page, "stablediffusion-model"); test("the listing alone fetches no variant descriptions", async ({ page }) => { // The whole point of the companion endpoint: a page load costs zero // probes no matter how many entries declare variants. await expect(railItems(page).first()).toBeVisible(); expect(variantUrls).toHaveLength(0); }); test("an entry without variants fetches nothing when selected", async ({ page, }) => { await plainRow(page).click(); await expect(page.locator(PANE)).toBeVisible(); expect(variantUrls).toHaveLength(0); }); test("plain Install sends no variant parameter", async ({ page }) => { await plainRow(page).click(); await page.locator('[data-testid="discover-install"]').click(); await expect.poll(() => installUrls.length).toBe(1); expect(installUrls[0]).not.toContain("variant="); }); test("the expanded detail row lists every variant", async ({ page }) => { await variantRow(page).click(); const detail = page.locator(PANE); await expect(detail).toContainText("Variants"); await expect(detail).toContainText("llama-model-q8"); await expect(detail).toContainText("llama-model-mlx"); await expect(detail).toContainText("llama-model-f16"); await expect(detail).toContainText("Unknown size"); await expect(detail).toContainText("Auto-selected"); await expect(detail).toContainText("Base build"); await expect(detail).toContainText("Does not fit"); await expect(detail).toContainText("mlx"); // Expanding is the second trigger point, so it pays for exactly one fetch. expect(variantUrls).toHaveLength(1); }); test("the variant rows line up as columns", async ({ page }) => { await variantRow(page).click(); const rows = page.locator(".variant-row"); await expect(rows).toHaveCount(4); const columns = await rows.evaluateAll((els) => els.map((el) => ({ backend: el.querySelector(".variant-row__backend").getBoundingClientRect().x, size: el.querySelector(".variant-row__size").getBoundingClientRect().right, })), ); // Names differ in length, so without shared tracks each row would start // its backend at a different x. Sub-pixel rounding is the only tolerance. for (const c of columns) { expect(Math.abs(c.backend - columns[0].backend)).toBeLessThan(1.5); expect(Math.abs(c.size - columns[0].size)).toBeLessThan(1.5); } }); test("only the informative status is badged", async ({ page }) => { await variantRow(page).click(); const detail = page.locator(PANE); await expect(detail.locator(".variant-row")).toHaveCount(4); // "Fits" was true of three rows out of four and said nothing; the row that // does not fit is the one worth marking. await expect(detail.getByText("Fits", { exact: true })).toHaveCount(0); const unfit = detail.locator(".variant-row--unfit"); await expect(unfit).toHaveCount(1); await expect(unfit).toContainText("llama-model-f16"); await expect(unfit.locator(".badge-warning")).toHaveText("Does not fit"); // Auto-selected still answers "what do I get if I just hit Install". await expect( detail.locator(".variant-row", { hasText: "llama-model-q8" }), ).toContainText("Auto-selected"); }); test("selecting an entry fetches its variants once and reuses them", async ({ page, }) => { // Selection is now the only trigger point, so it must pay for exactly one // probe however many times the pane is opened. await railItem(page, "llama-model").click(); await expect(page.locator(PANE)).toContainText("llama-model-q8"); await expect.poll(() => variantUrls.length).toBe(1); expect(variantUrls[0]).toContain("/api/models/variants/llama-model"); await railItem(page, "stablediffusion-model").click(); await railItem(page, "llama-model").click(); await expect(page.locator(PANE)).toContainText("llama-model-q8"); expect(variantUrls).toHaveLength(1); }); test("the pane says the variants are loading rather than opening empty", async ({ page, }) => { let unblock; releaseVariants = new Promise((resolve) => { unblock = resolve; }); await railItem(page, "llama-model").click(); await expect(page.locator(PANE)).toContainText("Loading variants"); unblock(); await expect(page.locator(PANE)).toContainText("llama-model-q8"); await expect(page.locator(PANE)).not.toContainText("Loading variants"); }); test("a variant that does not fit is still installable", async ({ page }) => { // Marked, not disabled: an explicit choice is an override the server // honours with a warning, and only the user knows they meant it. await railItem(page, "llama-model").click(); const unfit = page.locator(".variant-row--unfit"); await expect(unfit).toHaveCount(1); await expect(unfit).toContainText("llama-model-f16"); await expect(unfit).toBeEnabled(); await unfit.click(); await expect.poll(() => installUrls.length).toBe(1); expect(installUrls[0]).toContain("variant=llama-model-f16"); }); test("clicking a variant row installs that variant", async ({ page }) => { await variantRow(page).click(); await page .locator(".variant-row", { hasText: "llama-model-mlx" }) .click(); await expect.poll(() => installUrls.length).toBe(1); expect(installUrls[0]).toContain("variant=llama-model-mlx"); }); test("the detail row gives quantization its own column", async ({ page }) => { await variantRow(page).click(); const detail = page.locator(".variant-list"); await expect( detail.locator(".variant-row", { hasText: "llama-model-q8" }).locator(".variant-row__quant"), ).toHaveText("Q8_0"); await expect( detail.locator(".variant-row", { hasText: "llama-model-f16" }).locator(".variant-row__quant"), ).toHaveText("F16"); }); test("the detail row states an unknown quantization rather than leaving a gap", async ({ page, }) => { // An empty cell in an aligned column reads as a rendering fault, so the // absent case is spelled out and styled as the exception it is. await variantRow(page).click(); const cell = page .locator(".variant-row", { hasText: "llama-model-mlx" }) .locator(".variant-row__quant"); await expect(cell).toHaveText("Unknown format"); await expect(cell).toHaveClass(/variant-row__quant--unknown/); }); test("the detail row spells out the serving feature", async ({ page }) => { // This is the room the detail row has over the dropdown: "DFLASH" names // nothing to a user who has not met it. await variantRow(page).click(); await expect( page .locator(".variant-row", { hasText: "llama-model-q8" }) .locator(".badge", { hasText: "Faster: DFlash" }), ).toBeVisible(); // A build declaring no feature carries no feature badge at all. await expect( page .locator(".variant-row", { hasText: "llama-model-mlx" }) .locator(".badge", { hasText: "Faster" }), ).toHaveCount(0); }); test("a variant row is reachable and actionable from the keyboard", async ({ page, }) => { await variantRow(page).click(); const row = page.locator(".variant-row", { hasText: "llama-model-f16" }); await row.focus(); // A build that does not fit stays installable: the explicit choice is an // override the server honours. await expect(row).toBeFocused(); await page.keyboard.press("Enter"); await expect.poll(() => installUrls.length).toBe(1); expect(installUrls[0]).toContain("variant=llama-model-f16"); }); }); // The gallery entries behind two of llama-model's variants, as the listing // returns them when asked for one by exact name. Every field is deliberately // unlike the parent's, so a test asserting on them proves the panel resolved // the variant's own entry rather than re-rendering the row it sits under. // // llama-model-mlx is absent on purpose: it stands for a name the listing no // longer returns, which is a real outcome once a gallery is reloaded between // describing an entry's variants and asking about one of them. const VARIANT_ENTRIES = { "llama-model-q8": { name: "llama-model-q8", description: "The eight-bit build, kept for quality-sensitive work.", backend: "llama-cpp", installed: false, license: "q8-only-licence", tags: ["chat", "q8-only-tag"], urls: ["https://example.invalid/llama-model-q8"], additionalFiles: [ { filename: "llama-model-q8.gguf", uri: "https://example.invalid/q8.gguf", sha256: "q8", }, ], }, "llama-model-f16": { name: "llama-model-f16", description: "The full-precision build.", backend: "llama-cpp", installed: false, license: "f16-only-licence", tags: ["chat"], urls: [], }, }; // The variant list answers "how do these differ". This answers "tell me // everything about this one", for a build that has no listing row of its own // while the collapse is on and so is unreachable anywhere else in the page. test.describe("Models Gallery - Variant details", () => { let installUrls; // Requests for a single variant's gallery entry, told apart from the // gallery's own listing by the page size the detail lookup asks for. let detailUrls; test.beforeEach(async ({ page }) => { installUrls = []; detailUrls = []; await page.route("**/api/models*", (route) => { const url = new URL(route.request().url()); const term = (url.searchParams.get("term") || "").trim(); const isDetailLookup = url.pathname.endsWith("/api/models") && url.searchParams.get("items") === "100" && term !== ""; if (isDetailLookup) { detailUrls.push(url.toString()); const entry = VARIANT_ENTRIES[term]; return route.fulfill({ contentType: "application/json", body: JSON.stringify({ ...MOCK_MODELS_RESPONSE, models: entry ? [entry] : [], }), }); } return route.fulfill({ contentType: "application/json", body: JSON.stringify(MOCK_MODELS_RESPONSE), }); }); await page.route("**/api/models/install/**", (route) => { installUrls.push(route.request().url()); return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ jobID: "variant-install" }), }); }); await page.route("**/api/models/variants/**", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(MOCK_VARIANTS_RESPONSE), }), ); await page.goto("/app/models"); await railReady(page); // Expanding the parent is what puts the variant list on screen. await railItem(page, "llama-model").click(); await expect(page.locator(".variant-row")).toHaveCount(4); }); // Exact, because the names are prefixes of one another: "llama-model" is a // substring of every other variant's control. const infoFor = (page, variant) => page.getByRole("button", { name: `Show full details for ${variant}`, exact: true, }); test("every variant carries its own details control", async ({ page }) => { await expect(page.locator(".variant-row__info")).toHaveCount(4); // The accessible name has to say which build it acts on: a column of // identical "info" buttons is useless to a screen reader user. for (const variant of [ "llama-model", "llama-model-q8", "llama-model-mlx", "llama-model-f16", ]) { await expect(infoFor(page, variant)).toBeVisible(); } }); test("no details are fetched until the control is used", async ({ page }) => { // Expanding lists four variants. If the panel prefetched, this would be // four requests for data nobody has asked to see. expect(detailUrls).toHaveLength(0); await infoFor(page, "llama-model-q8").click(); await expect.poll(() => detailUrls.length).toBe(1); expect(detailUrls[0]).toContain("term=llama-model-q8"); }); test("the details shown are the variant's own entry, not the parent's", async ({ page, }) => { await infoFor(page, "llama-model-q8").click(); const panel = page.locator(".variant-detail"); await expect(panel).toContainText( "The eight-bit build, kept for quality-sensitive work.", ); await expect(panel).toContainText("q8-only-licence"); await expect(panel).toContainText("q8-only-tag"); await expect(panel).toContainText("https://example.invalid/llama-model-q8"); await expect(panel).toContainText("1 file"); // The parent's own description renders in the same expanded row. If the // panel were re-rendering the parent, this would be here too. await expect(panel).not.toContainText("A llama model"); }); test("a variant's details never nest another variants list", async ({ page, }) => { // Two levels of disclosure is already deep. A picker inside a picker is // where it stops being legible. await infoFor(page, "llama-model-q8").click(); await expect(page.locator(".variant-detail")).toBeVisible(); await expect(page.locator(".variant-detail .variant-list")).toHaveCount(0); }); test("opening the details does not install anything", async ({ page }) => { await infoFor(page, "llama-model-q8").click(); await expect(page.locator(".variant-detail")).toContainText( "q8-only-licence", ); // The panel is fully rendered by now, so an install triggered by the same // click would have fired. expect(installUrls).toHaveLength(0); }); test("the variant row still installs with the control alongside it", async ({ page, }) => { await page.locator(".variant-row", { hasText: "llama-model-q8" }).click(); await expect.poll(() => installUrls.length).toBe(1); expect(installUrls[0]).toContain("variant=llama-model-q8"); // And installing is not a request for details either. expect(detailUrls).toHaveLength(0); }); test("a variant whose entry cannot be resolved says so", async ({ page }) => { await infoFor(page, "llama-model-mlx").click(); const panel = page.locator(".variant-detail"); // Visibly degraded, not silently blank: an empty panel reads as a // rendering fault rather than as a lookup that came back with nothing. await expect(panel).toContainText( "Details for llama-model-mlx could not be loaded.", ); await expect(panel.locator(".variant-detail__state--error")).toBeVisible(); }); test("the details are fetched once and reused", async ({ page }) => { await infoFor(page, "llama-model-q8").click(); await expect.poll(() => detailUrls.length).toBe(1); await page .getByRole("button", { name: "Hide full details for llama-model-q8", exact: true, }) .click(); await expect(page.locator(".variant-detail")).toHaveCount(0); await infoFor(page, "llama-model-q8").click(); await expect(page.locator(".variant-detail")).toContainText( "q8-only-licence", ); expect(detailUrls).toHaveLength(1); }); test("only one variant's details are open at a time", async ({ page }) => { // The list is a comparison; two open panels push the rows being compared // apart. await infoFor(page, "llama-model-q8").click(); await infoFor(page, "llama-model-f16").click(); await expect(page.locator(".variant-detail")).toHaveCount(1); await expect(page.locator(".variant-detail")).toContainText( "f16-only-licence", ); }); test("the control is keyboard reachable, activates, and dismisses", async ({ page, }) => { const info = infoFor(page, "llama-model-q8"); // The pane is opened by clicking a rail entry, which is a real