import { expect, test, type Page } from "@playwright/test"; import { canonicalWorkspaceFixture, workspaceRevisionFixture, workspaceSummaryFixture } from "../src/test/workspace-fixtures"; import { memoryFormattingExamples } from "../src/shell/memoryFormattingExamples"; // These tests never forward API requests to an installation or invoke a real model. const workspace = "visual-fixture"; const title = "Clinical research workspace"; const model = { provider: "test", id: "review-model", name: "Review model", reasoning: true }; const revision = workspaceRevisionFixture(workspace); const evidence = { id: "evidence:cohort", title: "Cohort definition", kind: "domain", language: "en", purposes: ["sql_generation"], applies_to: { concepts: ["cohort"], tables: [], columns: [] }, payload: { rule: "# Approved cohort\n\nInclude eligible patients. Review the inclusion criteria before generating SQL." }, provenance: { source_file: "research-guide.md", supporting_excerpts: ["Eligibility is evaluated at admission."] }, review_items: [], file: "curated/domain/cohort.md", status: "active", revision: "1", }; const evidencePage = { items: [evidence], total: 1, page: 1, page_size: 25, errors: [], initialized: true, active_revision: "1", pending_revision: null, source_reviews: [], location: { repository: "/installation/workspaces", workspace: "/installation/workspaces/clinical-research", runtime_workspace: "/data/workspaces/clinical-research", host: "installation", command: "tht workspace consolidate", git_commands: ["git status", "git diff"] }, }; async function fixtures(page: Page, mode: "full" | "embedded" = "full") { const writes: string[] = []; await page.route("**/config.js", route => route.fulfill({ contentType: "application/javascript", body: `window.__THOTHII_CONFIG__ = ${JSON.stringify({ backendBaseUrl: "/api", shell: { mode, defaultLocale: "en" } })}; ${mode === "embedded" ? ` document.documentElement.setAttribute("data-bs-theme", "light"); const language = document.createElement("select"); language.className = "omics-language-select"; language.dataset.lang = "en"; language.hidden = true; document.body.prepend(language); ` : ""}`, })); await page.route("http://127.0.0.1:5183/api/**", async route => { const path = new URL(route.request().url()).pathname.replace(/^\/api/, ""); if (route.request().method() !== "GET") { writes.push(path); return route.fulfill({ status: 503, json: { message: "Visual fixture: no writes" } }); } let body: unknown; if (path === "/auth/config") body = { mode: "mock", localLogin: false, oidcLogin: false }; else if (path === "/me") body = { issuer: "test", subject: "admin", displayName: "Review administrator", isAdmin: true, roles: ["admin"], permissions: ["session.use", "session.read_all", "session.manage_all", "database.manage", "workspace.manage", "workspace.secrets.manage", "memory.manage", "evidence.manage", "pi.manage"], csrfToken: null, session: null }; else if (path === "/settings") body = { workspace, provider: model.provider, model: model.id, thinking: "high" }; else if (path === "/models") body = { models: [model] }; else if (path === "/workspaces") body = [workspaceSummaryFixture(workspace, { displayName: title })]; else if (path === `/workspaces/${workspace}`) body = { workspace: canonicalWorkspaceFixture(workspace), revision }; else if (path.endsWith("/runtime-configuration")) body = { workspaceId: workspace, revision, configurationState: "ready", requirements: [] }; else if (path.endsWith("/preprocessing")) body = { schemaVersion: 1, workspaceId: workspace, state: "ready", actionable: true, clearable: true, detail: "Catalog revision 1 is indexed.", metadataRevision: 1, preprocessedMetadataRevision: 1 }; else if (path === "/workspace-registry/status") body = { branch: "main", ahead: 0, behind: 0, degraded: false }; else if (path.endsWith("/memory/pending")) body = []; else if (path.endsWith("/memory")) body = { items: [], total: 0, page: 1, page_size: 25 }; else if (path.endsWith("/evidence")) body = evidencePage; else if (path.includes("/evidence/")) body = { ...evidencePage, item: evidence }; else if (path === "/pi-management/status") body = { ready: true, credentials: "present", hostPlatform: "macos", version: "0.80.3", config: { provider: model.provider, model: model.id, reasoning: "high" } }; else if (path === "/sessions") body = [{ id: "archived-fixture", status: "finalized", question: "How many eligible patients?", name: "Cohort review", archived: true, active: false, workspace, created_at: "2026-09-01T12:00:00Z", updated_at: null, author: null, group: null, summary: null }]; else if (path.endsWith("/documents")) body = [{ key: "question", title: "Original question", phase: "F1", format: "markdown", content: "# Cohort review\n\nHow many eligible patients were admitted in 2025?" }]; else if (path === "/health/dwh") body = { ok: true, detail: "Available" }; else if (path === "/catalog/databases") body = []; else if (path === "/catalog/metrics") body = { scope: "global", databaseId: null, tables: 0, columns: 0, sensitiveColumns: 0, relationships: 0, descriptionTargets: 0, describedTargets: 0, descriptionCoverage: 0, updatedAt: null }; else if (path === "/catalog/metadata-generation/models") body = { models: [], default: null }; else if (path.endsWith("-runs")) body = []; else return route.fulfill({ status: 404, json: { message: `Missing visual fixture: ${path}` } }); return route.fulfill({ json: body }); }); return writes; } async function navigation(page: Page) { const trigger = page.getByRole("button", { name: "Navigation", exact: true }); if (await trigger.isVisible()) await trigger.click(); } async function admin(page: Page, name: string) { await navigation(page); const toggle = page.getByRole("button", { name: "Administration", exact: true }); if (await toggle.getAttribute("aria-expanded") !== "true") await toggle.click(); await page.getByRole("button", { name, exact: true }).click(); } async function inspect(page: Page) { return page.evaluate(() => { const visible = (e: Element) => { const box = e.getBoundingClientRect(); return box.width > 0 && box.height > 0 && getComputedStyle(e).visibility !== "hidden" && !e.closest("[hidden]"); }; return { overflow: document.documentElement.scrollWidth - innerWidth, serif: [...document.querySelectorAll("h1,h2,h3,h4,button,label")].filter(visible) .filter(e => /Fraunces|Georgia|Times|ui-serif/.test(getComputedStyle(e).fontFamily)).map(e => e.textContent?.slice(0,60)), tiny: [...document.querySelectorAll("h1,h2,h3,h4,button,label,small")].filter(visible) .filter(e => parseFloat(getComputedStyle(e).fontSize) < 12).map(e => e.textContent?.slice(0,60)), }; }); } for (const width of [390, 1280]) { test(`session navigation has readiness dot and bounded accordion lists at ${width}px`, async ({ page }, testInfo) => { await page.setViewportSize({ width, height: 1000 }); const writes = await fixtures(page); const sessions = Array.from({ length: 80 }, (_, index) => ({ id: `nav-${index}`, name: `Session ${index}`, question: `Question ${index}`, archived: index >= 40, active: false, status: index >= 40 ? "finalized" : "open", workspace, group: null, author: null, summary: null, created_at: "2026-09-01T12:00:00Z", updated_at: null, })); await page.route("**/api/sessions?*", route => route.fulfill({ json: sessions })); await page.route("**/api/sessions", route => route.fulfill({ json: sessions })); await page.goto("/"); await navigation(page); await page.getByRole("button", { name: "Administration", exact: true }).click(); const workspaceButton = page.getByRole("button", { name: "Workspace", exact: true }); await expect(workspaceButton.getByRole("img")).toHaveAttribute("data-ready", "true"); await expect(workspaceButton).toHaveAccessibleDescription("Workspace readiness: ready"); await expect(page.getByText("Workspace readiness: ready", { exact: true })).toHaveCount(0); const green = await workspaceButton.getByRole("img").evaluate(el => getComputedStyle(el).backgroundColor); const active = page.getByRole("button", { name: "Active sessions", exact: true }); const archive = page.getByRole("button", { name: "Archive (40)", exact: true }); await expect(active).toHaveAttribute("aria-expanded", "false"); await expect(archive).toHaveAttribute("aria-expanded", "false"); await expect(page.getByRole("checkbox", { name: "Select all" })).toHaveCount(0); await expect(page.getByText("Sessions", { exact: true })).toHaveCount(0); const activeBox = await active.boundingBox(); const archiveBox = await archive.boundingBox(); expect(archiveBox!.y - activeBox!.y - activeBox!.height).toBeLessThan(8); await page.screenshot({ path: testInfo.outputPath("session-accordions-closed.png"), animations: "disabled" }); for (const name of ["Active sessions", "Archive (40)"]) { await page.getByRole("button", { name, exact: true }).click(); await expect(name === "Active sessions" ? archive : active).toHaveAttribute("aria-expanded", "false"); await expect(page.locator(".thot-session-accordion__panel:visible")).toHaveCount(1); const panel = page.getByRole("region", { name, exact: true }); await expect(panel.getByRole("checkbox", { name: "Select all" })).toBeVisible(); await expect(panel).toHaveCSS("overflow-y", "auto"); const dimensions = await panel.evaluate(el => ({ height: el.getBoundingClientRect().height, scroll: el.scrollHeight, client: el.clientHeight })); expect(dimensions.height).toBeLessThanOrEqual(288); expect(dimensions.client).toBeGreaterThan(30); expect(dimensions.scroll).toBeGreaterThan(dimensions.client); const box = await panel.boundingBox(); expect(box!.y + box!.height).toBeLessThanOrEqual(1000); await panel.evaluate(el => { el.scrollTop = el.scrollHeight; }); expect(await panel.evaluate(el => el.scrollTop)).toBeGreaterThan(0); } await page.screenshot({ path: testInfo.outputPath("session-accordions.png"), animations: "disabled" }); await expect(archive).toBeInViewport(); expect((await inspect(page)).overflow).toBe(0); // A refetch failure must not keep a stale green signal. await page.route("**/api/workspaces/*/preprocessing", route => route.fulfill({ status: 503, json: { error: "Fixture unavailable" } })); await expect(workspaceButton.getByRole("img")).toHaveAttribute("data-ready", "false", { timeout: 15000 }); await expect(workspaceButton).toHaveAccessibleDescription("Workspace readiness: unavailable"); expect(await workspaceButton.getByRole("img").evaluate(el => getComputedStyle(el).backgroundColor)).not.toBe(green); expect(writes).toEqual([]); }); } for (const width of [390, 1280, 2400]) { test(`knowledge readers use available width and readable paragraphs at ${width}px`, async ({ page }, testInfo) => { await page.setViewportSize({ width, height: 1100 }); const writes = await fixtures(page); const sample = memoryFormattingExamples[0]; const longEvidence = { ...evidence, applies_to: { concepts: ["Esempio simulato"], tables: ["demo_visuale.identificatore_lungo_per_la_verifica_della_formattazione"], columns: [] }, payload: { rule: sample.detail + "\n\n" + [1, 2, 3, 4, 5, 6].map(level => "#".repeat(level) + " Sottotitolo " + level + "\n\nTesto della sottosezione.").join("\n\n") + "\n\n```sql\nSELECT 1;\n```" }, provenance: { source_file: "source/demo.md", supporting_excerpts: ["**Esempio simulato** con `codice_inline` e un collegamento alla regola.\n\nSecondo paragrafo di provenienza."] } }; await page.route(`**/api/workspaces/${workspace}/evidence/*`, route => route.fulfill({ json: { ...evidencePage, item: longEvidence } })); await page.goto("/"); await admin(page, "Evidence"); await page.getByRole("button", { name: evidence.title, exact: true }).click(); const evidenceDetail = page.getByRole("region", { name: "Evidence detail" }); const article = evidenceDetail.locator("article"); await expect(article).toHaveCSS("max-width", "none"); const rule = article.locator(".thot-knowledge-prose").first(); await expect(rule.locator(":scope > p")).not.toHaveCount(1); await expect(rule.locator("p").first()).toHaveCSS("max-width", "none"); await expect(article.locator(".thot-knowledge-title")).toHaveCSS("font-size", "24px"); for (const heading of await article.locator(".thot-knowledge-heading").all()) { await expect(heading).toHaveCSS("font-size", "20px"); await expect(heading).toHaveCSS("font-weight", "600"); await expect(heading).toHaveCSS("margin-bottom", "8px"); } const bodyFamily = await rule.evaluate(el => getComputedStyle(el).fontFamily); expect(bodyFamily).toContain("Manrope"); for (const paragraph of await article.locator("section > p:not(.thot-knowledge-meta), .thot-knowledge-prose p").all()) { await expect(paragraph).toHaveCSS("font-size", "16px"); await expect(paragraph).toHaveCSS("line-height", "26.4px"); await expect(paragraph).toHaveCSS("font-family", bodyFamily); } for (const heading of await rule.locator("h1,h2,h3,h4,h5,h6").all()) { await expect(heading).toHaveCSS("font-size", "16px"); await expect(heading).toHaveCSS("font-weight", "600"); await expect(heading).toHaveCSS("font-family", bodyFamily); } await expect(rule.locator("pre code")).toHaveCSS("font-size", "14px"); await expect(rule.locator("p code").first()).toHaveCSS("font-size", "14px"); expect(await article.evaluate(el => { const parent = el.parentElement!; const css = getComputedStyle(parent); return Math.abs(el.getBoundingClientRect().width - (parent.clientWidth - parseFloat(css.paddingLeft) - parseFloat(css.paddingRight))); })).toBeLessThan(2); const copy = article.getByRole("button", { name: "Copy Evidence file path", exact: true }); await expect(copy).toHaveText(""); await expect(copy.locator("svg")).toHaveCount(1); await expect(article.locator("strong").filter({ hasText: "Esempio simulato" })).toBeVisible(); await article.locator(".thot-knowledge-title").scrollIntoViewIfNeeded(); await page.screenshot({ path: testInfo.outputPath("evidence-readable.png"), animations: "disabled" }); expect((await inspect(page)).overflow).toBe(0); await admin(page, "Memory"); await page.getByRole("button", { name: sample.subject, exact: true }).click(); const memoryDetail = page.getByRole("region", { name: "Memory detail" }); await expect(memoryDetail.getByRole("note")).toContainText("Not saved, indexed or sent to the model."); const content = memoryDetail.locator(".thot-knowledge-prose").first(); await expect(content.locator(":scope > p")).not.toHaveCount(1); await expect(content.locator("p").first()).toHaveCSS("max-width", "none"); await expect(content.locator("p").nth(1)).toHaveCSS("margin-top", "20px"); await expect(memoryDetail.locator(".thot-knowledge-title")).toHaveCSS("font-size", "24px"); for (const heading of await memoryDetail.locator(".thot-knowledge-heading").all()) { await expect(heading).toHaveCSS("font-size", "20px"); await expect(heading).toHaveCSS("font-weight", "600"); await expect(heading).toHaveCSS("font-family", bodyFamily); await expect(heading).toHaveCSS("margin-bottom", "8px"); } await expect(content.locator("p").first()).toHaveCSS("font-size", "16px"); await expect(content.locator("p").first()).toHaveCSS("line-height", "26.4px"); await expect(content.locator("p").first()).toHaveCSS("font-family", bodyFamily); expect(await content.evaluate(el => Math.abs(el.getBoundingClientRect().width - el.parentElement!.clientWidth))).toBeLessThan(2); await memoryDetail.getByRole("heading", { name: sample.subject, exact: true }).scrollIntoViewIfNeeded(); await page.screenshot({ path: testInfo.outputPath("memory-readable.png"), animations: "disabled" }); await expect(memoryDetail.getByRole("button", { name: "Edit card", exact: true })).toHaveCount(0); expect((await inspect(page)).overflow).toBe(0); await page.getByRole("button", { name: "Use dark theme", exact: true }).click(); await expect(page.locator(".thot-host")).toHaveAttribute("data-theme", "dark"); await memoryDetail.getByRole("heading", { name: sample.subject, exact: true }).scrollIntoViewIfNeeded(); await page.screenshot({ path: testInfo.outputPath("memory-readable-dark.png"), animations: "disabled" }); await expect(content.locator("p").first()).toHaveCSS("line-height", "26.4px"); await page.getByRole("button", { name: memoryFormattingExamples[1].subject, exact: true }).click(); await expect(memoryDetail.getByRole("heading", { name: "Prima del conteggio" })).toHaveCSS("font-size", "16px"); await expect(memoryDetail.getByRole("heading", { name: "Prima del conteggio" })).toHaveCSS("font-family", bodyFamily); await memoryDetail.locator(".thot-knowledge-title").scrollIntoViewIfNeeded(); await page.screenshot({ path: testInfo.outputPath("memory-headings-dark.png"), animations: "disabled" }); await page.getByRole("button", { name: memoryFormattingExamples[2].subject, exact: true }).click(); await expect(memoryDetail.locator("pre")).toHaveCSS("font-size", "14px"); expect((await inspect(page)).overflow).toBe(0); expect(writes).toEqual([]); }); } for (const width of [390, 1280]) { test(`full shell keeps the visual revision across language and theme changes at ${width}px`, async ({ page }, testInfo) => { await page.setViewportSize({ width, height: 1000 }); const writes = await fixtures(page); await page.goto("/"); const header = page.locator(".thot-full-header"); await expect(header).toBeVisible(); await expect(header).toHaveCSS("background-color", "rgb(203, 51, 59)"); expect(await header.locator(".thot-full-header__brand span").evaluate(el => getComputedStyle(el).color === getComputedStyle(el.closest("header")!).color, )).toBe(true); await page.evaluate(() => document.fonts.ready); expect(await page.evaluate(() => [...document.fonts].some(font => font.family.includes("Manrope Variable") && font.status === "loaded", ))).toBe(true); const layout = await page.locator(".thot-context-layout").boundingBox(); expect(layout!.x).toBeGreaterThanOrEqual(20); expect(Math.abs(layout!.x - (width - layout!.x - layout!.width))).toBeLessThan(1); await admin(page, "Database"); await expect(page.getByRole("heading", { name: "Database management", exact: true })).toBeVisible(); await expect(page.locator(".thot-fleet-ledger__header-status")).toHaveCSS("padding-top", "16px"); await page.getByRole("button", { name: /Working context/ }).click(); const fields = page.locator(".thot-context-fields"); const done = fields.getByRole("button", { name: "Done", exact: true }); await expect(done).toBeVisible(); if (width >= 1280) { const workspaceBox = await fields.getByRole("combobox", { name: "Workspace", exact: true }).boundingBox(); const modelBox = await fields.getByRole("combobox", { name: "Model", exact: true }).boundingBox(); const doneBox = await done.boundingBox(); expect(Math.abs(workspaceBox!.y - modelBox!.y)).toBeLessThan(1); expect(Math.abs(modelBox!.y - doneBox!.y)).toBeLessThan(1); expect(doneBox!.x).toBeGreaterThan(modelBox!.x + modelBox!.width); for (const name of ["My sessions", "All sessions"]) { await expect(page.getByRole("tab", { name, exact: true })).toHaveCSS("border-bottom-width", "1px"); } } expect(await inspect(page)).toEqual({ overflow: 0, serif: [], tiny: [] }); await page.screenshot({ path: testInfo.outputPath("full-context-light.png"), animations: "disabled" }); await done.click(); await header.getByRole("combobox").selectOption("it"); await expect(page.getByRole("heading", { name: "Gestione database", exact: true })).toBeVisible(); await expect(page.locator(".thot-administration-eyebrow")).toHaveText("Amministrazione"); await expect(page.locator(".thot-context-summary small")).toHaveText("Contesto di lavoro"); await page.getByRole("button", { name: "Usa il tema scuro", exact: true }).click(); await expect(page.locator(".thot-host")).toHaveAttribute("data-theme", "dark"); await expect(header).toHaveCSS("background-color", "rgb(203, 51, 59)"); await page.screenshot({ path: testInfo.outputPath("full-header-dark.png"), animations: "disabled" }); expect(await inspect(page)).toEqual({ overflow: 0, serif: [], tiny: [] }); await page.reload(); await expect(page.getByRole("heading", { name: "Gestione database", exact: true })).toBeVisible(); await expect(header.getByRole("combobox")).toHaveValue("it"); await expect(page.locator(".thot-host")).toHaveAttribute("data-theme", "dark"); await page.getByRole("button", { name: "Usa il tema chiaro", exact: true }).click(); await header.getByRole("combobox").selectOption("en"); await expect(page.getByRole("heading", { name: "Database management", exact: true })).toHaveCSS("font-size", "24px"); expect(await inspect(page)).toEqual({ overflow: 0, serif: [], tiny: [] }); expect(writes).toEqual([]); }); } test("session tabs have padded labels and matching one-pixel borders on every side", async ({ page }, testInfo) => { await page.setViewportSize({ width: 1280, height: 1000 }); await fixtures(page); await page.goto("/?thoth_route=administration%2Fdatabase"); const header = page.locator(".thot-full-header"); for (const locale of ["en", "it"]) { await header.getByRole("combobox").selectOption(locale); await expect(page.getByRole("button", { name: locale === "it" ? "Sessione" : "Session", exact: true })).toHaveCount(1); await expect(page.getByRole("button", { name: /^(New session|Return to session|Nuova sessione|Torna alla sessione)$/ })).toHaveCount(0); const tabs = page.locator('[role="tablist"]').getByRole("tab"); for (const theme of ["light", "dark"]) { if (theme === "dark") await header.getByRole("button", { name: locale === "it" ? "Usa il tema scuro" : "Use dark theme", exact: true }).click(); for (const selected of [0, 1]) { await tabs.nth(selected).click(); await expect(tabs.nth(selected)).toHaveAttribute("aria-selected", "true"); for (const tab of await tabs.all()) { await expect(tab).toHaveCSS("border-bottom-width", "1px"); const borders = await tab.evaluate(el => { const css = getComputedStyle(el); return { widths: [css.borderTopWidth, css.borderRightWidth, css.borderBottomWidth, css.borderLeftWidth], colors: [css.borderTopColor, css.borderRightColor, css.borderBottomColor, css.borderLeftColor], paddingX: css.paddingLeft, paddingY: css.paddingTop, fits: el.scrollHeight <= el.clientHeight }; }); expect(borders.widths).toEqual(["1px", "1px", "1px", "1px"]); expect(new Set(borders.colors).size).toBe(1); expect(borders).toMatchObject({ paddingX: "11px", paddingY: "3px", fits: true }); } } await page.mouse.move(0, 0); await page.screenshot({ path: testInfo.outputPath(`session-tabs-${locale}-${theme}.png`), animations: "disabled" }); if (theme === "dark") await header.getByRole("button", { name: locale === "it" ? "Usa il tema chiaro" : "Use light theme", exact: true }).click(); } } }); for (const locale of ["en", "it"]) { test(`login keeps only the main title in ${locale}`, async ({ page }, testInfo) => { await fixtures(page); await page.route("**/config.js", route => route.fulfill({ contentType: "application/javascript", body: `window.__THOTHII_CONFIG__ = ${JSON.stringify({ backendBaseUrl: "/api", shell: { mode: "full", defaultLocale: locale } })};` })); await page.route("**/api/auth/config", route => route.fulfill({ json: { mode: "local", localLogin: true, oidcLogin: false } })); await page.route("**/api/me", route => route.fulfill({ status: 401, json: { code: "unauthenticated" } })); await page.goto("/"); await expect(page.getByRole("heading", { name: locale === "it" ? "Accedi a ThothII" : "Sign in to ThothII" })).toBeVisible(); await expect(page.getByText(locale === "it" ? "Accesso a ThothII" : "ThothII access", { exact: true })).toHaveCount(0); await expect(page.getByText(locale === "it" ? "Usa l’account della tua installazione per continuare." : "Use your installation account to continue.", { exact: true })).toHaveCount(0); await page.screenshot({ path: testInfo.outputPath(`login-${locale}.png`), animations: "disabled" }); }); } test("language selector has no pointer ring and retains keyboard focus in both themes", async ({ page }, testInfo) => { await fixtures(page); await page.goto("/"); const language = page.getByRole("combobox", { name: "Interface language" }); for (const theme of ["light", "dark"]) { if (theme === "dark") await page.getByRole("button", { name: "Use dark theme", exact: true }).click(); await language.click(); await page.keyboard.press("Escape"); await expect(language).toBeFocused(); await expect(language).toHaveCSS("outline-style", "none"); await expect(language).toHaveCSS("box-shadow", "none"); await page.screenshot({ path: testInfo.outputPath(`language-pointer-${theme}.png`), animations: "disabled" }); await page.keyboard.press("Tab"); await page.keyboard.press("Shift+Tab"); await expect(language).toBeFocused(); await expect(language).toHaveCSS("outline-style", "solid"); } }); for (const route of ["/", "/?thoth_route=administration%2Fdatabase"]) { test(`Core prompt remains readable after loading ${route}`, async ({ page }, testInfo) => { const writes = await fixtures(page); await page.goto(route); if (route !== "/") { await navigation(page); await page.getByRole("button", { name: "Session", exact: true }).click(); } const prompt = page.getByRole("textbox", { name: "New question", exact: true }); await expect(prompt).toBeVisible(); await prompt.focus(); const readable = async () => { await expect.poll(() => prompt.evaluate(el => { const style = getComputedStyle(el); return el.clientHeight >= parseFloat(style.lineHeight) + parseFloat(style.paddingTop) + parseFloat(style.paddingBottom); })).toBe(true); }; await readable(); await expect(prompt).toHaveCSS("outline-style", "none"); await page.screenshot({ path: testInfo.outputPath("Prompt-focused.png") }); // Full-mode gutters can wrap the empty placeholder on a narrow viewport. // Compare clearing with that same viewport's initial height, not a single-line constant. await page.setViewportSize({ width: 390, height: 900 }); await readable(); const emptyHeight = await prompt.evaluate(el => el.clientHeight); await prompt.fill("First line"); await prompt.press("Shift+Enter"); await prompt.press("a"); await expect(prompt).toHaveValue("First line\na"); await expect.poll(() => prompt.evaluate(el => el.clientHeight)).toBeGreaterThan(40); const question = "Compare eligible patients across the available cohorts. ".repeat(30); await prompt.fill(question); await page.setViewportSize({ width: 390, height: 900 }); await readable(); await expect.poll(() => prompt.evaluate(el => el.clientHeight)).toBe(160); expect(await prompt.evaluate(el => el.scrollHeight > el.clientHeight)).toBe(true); await admin(page, "Database"); await navigation(page); await page.getByRole("button", { name: "Session", exact: true }).click(); await expect(prompt).toHaveValue(question); await readable(); await page.screenshot({ path: testInfo.outputPath("Prompt-multiline-mobile.png") }); await prompt.fill(""); await readable(); await expect.poll(() => prompt.evaluate(el => el.clientHeight)).toBe(emptyHeight); expect(writes).toEqual(route === "/" ? ["/runtime/prewarm"] : ["/runtime/prewarm", "/runtime/prewarm"]); }); } for (const width of [390, 649, 768, 1280, 1600]) { test(`all surfaces retain shared typography at ${width}px`, async ({ page }, testInfo) => { await page.setViewportSize({ width, height: 1000 }); const writes = await fixtures(page); await page.goto("/"); await expect(page.getByTestId("app-shell")).toBeVisible(); await expect(page.getByRole("button", { name: /Working context/ })).toContainText(title); for (const name of ["Workspace", "Database", "Memory", "Evidence", "Pi configuration"]) { await admin(page, name); const heading = page.getByRole("heading", { name: `${name === "Pi configuration" ? "Pi" : name} management`, exact: true }); await expect(heading).toBeVisible(); await expect(heading).toHaveCSS("font-size", "24px"); expect(await inspect(page)).toEqual({ overflow: 0, serif: [], tiny: [] }); if (name === "Memory") await expect(page.getByText("No memory cards yet.", { exact: false })).toBeVisible(); if (name === "Database") { const statuses = page.locator(".thot-fleet-ledger__header-status .thot-fleet-ledger__status-item"); await expect(statuses).toHaveCount(3); const catalog = await statuses.nth(0).boundingBox(); const operations = await statuses.nth(1).boundingBox(); const metadata = await statuses.nth(2).boundingBox(); const summary = await page.locator(".thot-fleet-ledger__kpi-section > h2").boundingBox(); const summaryBlock = await page.locator(".thot-fleet-ledger__kpi-section").boundingBox(); const navigationBlock = await page.locator(".thot-fleet-ledger__navigation").boundingBox(); const gridBlock = await page.locator(".thot-fleet-ledger__content").boundingBox(); const databasesLabel = await page.locator(".thot-fleet-ledger__navigation").getByText("Databases", { exact: true }).boundingBox(); const tablesLabel = await page.locator(".thot-fleet-ledger__kpi-label").getByText("Tables", { exact: true }).boundingBox(); expect(Math.abs(summaryBlock!.x - gridBlock!.x)).toBeLessThan(1); expect(Math.abs(navigationBlock!.x - gridBlock!.x)).toBeLessThan(1); expect(Math.abs(summary!.x - databasesLabel!.x)).toBeLessThan(1); expect(Math.abs(summary!.x - tablesLabel!.x)).toBeLessThan(1); const gridLabel = await page.locator(".thot-fleet-grid-toolbar").getByText("Workspace databases", { exact: true }).boundingBox(); expect(Math.abs(summary!.x - gridLabel!.x)).toBeLessThan(1); expect(Math.abs(summaryBlock!.width - gridBlock!.width)).toBeLessThan(1); expect(Math.abs(navigationBlock!.width - gridBlock!.width)).toBeLessThan(1); const contextTrigger = page.getByRole("button", { name: /Working context/ }); await contextTrigger.click(); const contextBody = page.locator(".thot-context-body"); await expect(contextBody).toBeVisible(); await expect(contextBody).not.toContainText("Your last choices take precedence"); await expect(contextBody).not.toContainText("Installation default"); await expect(contextBody).not.toContainText("Last choice"); const contextBlock = await contextBody.boundingBox(); const workspaceSelect = await contextBody.getByRole("combobox", { name: "Workspace", exact: true }).boundingBox(); expect(Math.abs(contextBlock!.x - summaryBlock!.x)).toBeLessThan(1); expect(Math.abs(workspaceSelect!.x - summary!.x)).toBeLessThan(1); expect(await inspect(page)).toEqual({ overflow: 0, serif: [], tiny: [] }); await page.screenshot({ path: testInfo.outputPath("Context-expanded.png") }); await contextBody.getByRole("button", { name: "Done", exact: true }).click(); expect(Math.abs(catalog!.x - summary!.x)).toBeLessThan(1); const workArea = await page.locator(".thot-fleet-ledger__workspace").boundingBox(); if (workArea!.width >= 768) { const coverage = await page.locator(".thot-fleet-ledger__kpi-label").filter({ hasText: "Description coverage" }).boundingBox(); expect(Math.abs(metadata!.x - coverage!.x)).toBeLessThan(1); expect(Math.abs(operations!.x - (catalog!.x + metadata!.x) / 2)).toBeLessThan(1); } for (const status of await statuses.all()) { const label = await status.locator(".thot-fleet-ledger__status-label > span:first-child").boundingBox(); const mark = await status.locator(".thot-fleet-ledger__status-mark").boundingBox(); const value = await status.locator(".thot-fleet-ledger__status-value").boundingBox(); expect(mark!.x - (label!.x + label!.width)).toBeGreaterThanOrEqual(7); expect(Math.abs(label!.x - value!.x)).toBeLessThan(1); } } if (name === "Evidence") { const search = await page.getByRole("textbox", { name: "Search Evidence" }).boundingBox(); expect(search!.width).toBeGreaterThan(180); } await page.screenshot({ path: testInfo.outputPath(`${name.replaceAll(" ", "-")}.png`) }); } await page.getByRole("button", { name: "Host maintenance", exact: true }).click(); await expect(page.getByRole("tab", { name: "macOS", exact: true })).toHaveAttribute("aria-selected", "true"); await expect(page.getByRole("region", { name: "Pi operator workflow" })).toHaveCSS("font-size", "16px"); await page.screenshot({ path: testInfo.outputPath("Pi-guide.png") }); await navigation(page); await page.getByRole("tab", { name: "All sessions", exact: true }).click(); await expect(page.locator(".thot-wordmark--sidebar")).toHaveCSS("font-size", "32px"); await page.getByRole("button", { name: /Archive \(1\)/ }).click(); await page.getByRole("button", { name: "Cohort review", exact: true }).click(); const documents = page.getByRole("complementary", { name: "Session summary" }); await expect(documents).toBeVisible(); await expect(documents.getByRole("heading", { name: "Original question" })).toBeVisible(); const bounds = await documents.boundingBox(); expect(bounds!.width).toBeGreaterThanOrEqual(Math.min(width, 300)); expect(await inspect(page)).toEqual({ overflow: 0, serif: [], tiny: [] }); await page.screenshot({ path: testInfo.outputPath("Sessions.png") }); await page.getByRole("button", { name: "Close panel", exact: true }).click(); await expect(page.getByRole("textbox", { name: "New question", exact: true })).toBeVisible(); await expect(page.locator(".thot-wordmark--core")).toHaveCSS("font-size", "48px"); await page.screenshot({ path: testInfo.outputPath("Core.png") }); expect(writes).toEqual([]); }); } test("theme follows the app, not an unrelated OS preference; keyboard tabs and dirty guard remain", async ({ page }, testInfo) => { await page.setViewportSize({ width: 1280, height: 900 }); const writes = await fixtures(page); await page.emulateMedia({ colorScheme: "dark" }); await page.goto("/"); await expect(page.getByTestId("app-shell")).toBeVisible(); await expect(page.getByRole("banner", { name: "Application header" })).toBeVisible(); await admin(page, "Pi configuration"); const testButton = page.getByRole("button", { name: "Test catalog default", exact: true }); await expect(testButton).toHaveCSS("background-color", "oklch(0.9985 0.0006 17.2)"); await page.getByRole("button", { name: "Use dark theme", exact: true }).click(); await expect(page.locator(".thot-context-trigger")).toHaveCSS("color", "oklch(0.931 0 90)"); await expect(testButton).toHaveCSS("background-color", "oklch(0.62 0.008 23.2 / 0.3)"); await page.screenshot({ path: testInfo.outputPath("Pi-dark.png") }); const guideButton = page.getByRole("button", { name: "Host maintenance", exact: true }); await guideButton.click(); const mac = page.getByRole("tab", { name: "macOS", exact: true }); await mac.focus(); await page.keyboard.press("ArrowRight"); await expect(page.getByRole("tab", { name: "Windows", exact: true })).toBeFocused(); await expect(page.getByRole("tab", { name: "Windows", exact: true })).toHaveAttribute("aria-selected", "true"); await admin(page, "Memory"); await page.getByRole("button", { name: "New card", exact: true }).click(); await page.getByRole("textbox", { name: "Title", exact: true }).fill("Keep this draft"); await page.getByRole("button", { name: "Session", exact: true }).click(); await expect(page.getByRole("textbox", { name: "Title", exact: true })).toHaveValue("Keep this draft"); await page.screenshot({ path: testInfo.outputPath("Memory-dark-unsaved.png") }); await page.getByRole("button", { name: "Cancel", exact: true }).click(); await page.getByRole("button", { name: "Session", exact: true }).click(); await expect(page.getByRole("textbox", { name: "New question", exact: true })).toBeVisible(); expect(writes).toEqual(["/runtime/prewarm"]); }); test("host header and left rail leave the application a bounded working area", async ({ page }, testInfo) => { await page.setViewportSize({ width: 1280, height: 1000 }); await fixtures(page, "embedded"); await page.goto("/"); await expect(page.getByTestId("app-shell")).toBeVisible(); await expect(page.locator(".thot-full-header")).toHaveCount(0); // Geometry fixture, not a replacement for acceptance inside the real Omics deployment. await page.addStyleTag({ content: ` body { margin: 0; } #root { margin-left: 240px; margin-top: 72px; --thoth-app-height: calc(100dvh - 72px); } .px-4 { padding-left: 2.25rem !important; padding-right: 2.25rem !important; } ` }); await admin(page, "Workspace"); const shell = await page.getByTestId("app-shell").boundingBox(); expect(shell!.x).toBe(240); expect(shell!.x + shell!.width).toBeLessThanOrEqual(1280); expect(shell!.y + shell!.height).toBeLessThanOrEqual(1000); expect(await inspect(page)).toEqual({ overflow: 0, serif: [], tiny: [] }); await page.screenshot({ path: testInfo.outputPath("Host-bounded-Workspace.png") }); await admin(page, "Evidence"); const search = await page.getByRole("textbox", { name: "Search Evidence" }).boundingBox(); expect(search!.width).toBeGreaterThan(180); expect(await inspect(page)).toEqual({ overflow: 0, serif: [], tiny: [] }); }); for (const width of [390, 1280]) { test(`embedded active session keeps steering and stop inside the portal at ${width}px`, async ({ page }, testInfo) => { await page.setViewportSize({ width, height: 822 }); await fixtures(page, "embedded"); await page.route("**/api/sessions", route => route.request().method() === "POST" ? route.fulfill({ json: { id: "layout-session" } }) : route.fallback()); await page.route("**/api/sessions/layout-session", route => route.fulfill({ json: { id: "layout-session", active: true, status: "open", workspace } })); await page.goto("/"); // The Omics template bounds #root below its topbar and clips overflow. await page.addStyleTag({ content: ` body { margin: 0; padding-top: 70px; } #root { height: calc(100dvh - 110px); overflow: hidden; } ` }); const prompt = page.getByRole("textbox", { name: "New question", exact: true }); await prompt.fill("Review the cohort"); await page.getByRole("button", { name: "Send", exact: true }).click(); const steering = page.getByRole("textbox", { name: "Steering", exact: true }); await expect(steering).toBeVisible(); // Drive the same store projection as streaming, without a real model. await page.evaluate(async () => { const path = "/src/store/sessionStore.ts"; const { useSessionStore } = await import(/* @vite-ignore */ path); useSessionStore.getState().applyEvent({ type: "text_delta", text: "Reviewing the cohort\n".repeat(80) }); }); await steering.fill("Please pause the analysis\n".repeat(15)); const stop = page.getByRole("button", { name: "Stop and save session", exact: true }); await page.locator("#root").evaluate(el => { el.scrollTop = 0; }); const root = await page.locator("#root").boundingBox(); for (const control of [steering, stop]) { const box = await control.boundingBox(); expect(box!.y).toBeGreaterThanOrEqual(root!.y); expect(box!.y + box!.height).toBeLessThanOrEqual(root!.y + root!.height); await expect(control).toBeInViewport(); } await page.screenshot({ path: testInfo.outputPath("embedded-session.png") }); await stop.click(); await expect(page.getByRole("dialog", { name: "Stop the session?" })).toBeVisible(); }); }