731 lines
47 KiB
TypeScript
731 lines
47 KiB
TypeScript
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();
|
||
}
|
||
|
||
for (const mode of ["full", "embedded"] as const) {
|
||
for (const viewport of [{ width: 1280, height: 720 }, { width: 390, height: 844 }]) {
|
||
test(`catalog configuration scrolls to all fields and actions in ${mode} at ${viewport.width}px`, async ({ page }, testInfo) => {
|
||
await page.setViewportSize(viewport);
|
||
const writes = await fixtures(page, mode);
|
||
await page.route("**/api/catalog/databases", route => route.fulfill({ json: [{
|
||
workspaceId: workspace, workspaceName: title, workspaceAvailable: true,
|
||
workspaceRevision: revision, workspaceEvidence: { sourceType: null, state: "not_declared" },
|
||
runtimeBinding: null, configured: false, engine: "postgres", databaseName: "warehouse",
|
||
schema: "public", version: 0, createdAt: "", updatedAt: "",
|
||
binding: { transport: "postgres_direct", port: 5432 }, connectionStatus: "untested",
|
||
secrets: { password: false, apiKey: false, sshPrivateKey: false,
|
||
sshPrivateKeyPassphrase: false, sshKnownHosts: false, tlsCa: false },
|
||
}] }));
|
||
await page.goto("/?thoth_route=administration/database");
|
||
await page.getByRole("button", { name: `Configure catalog for ${title}`, exact: true }).click();
|
||
const form = page.getByRole("region", { name: "Configure catalog form", exact: true });
|
||
await expect(form).toBeVisible();
|
||
const scrollArea = page.locator(".thot-fleet-ledger__workspace");
|
||
const box = await scrollArea.boundingBox();
|
||
expect(box).not.toBeNull();
|
||
await page.mouse.move(box!.x + box!.width / 2, box!.y + box!.height - 30);
|
||
await page.mouse.wheel(0, 3000);
|
||
await expect(form.getByRole("button", { name: "Save catalog configuration", exact: true })).toBeInViewport();
|
||
await expect(form.getByLabel("Password", { exact: true })).toBeInViewport();
|
||
expect(await scrollArea.evaluate(el => el.scrollTop)).toBeGreaterThan(0);
|
||
expect((await inspect(page)).overflow).toBe(0);
|
||
await page.screenshot({ path: testInfo.outputPath("catalog-configuration-bottom.png") });
|
||
await page.mouse.wheel(0, -3000);
|
||
await expect.poll(() => scrollArea.evaluate(el => el.scrollTop)).toBe(0);
|
||
await form.getByRole("button", { name: "Back to list", exact: true }).scrollIntoViewIfNeeded();
|
||
await form.getByRole("button", { name: "Back to list", exact: true }).click();
|
||
await expect(page.getByRole("button", { name: `Configure catalog for ${title}`, exact: true })).toBeVisible();
|
||
expect(writes).toEqual([]);
|
||
});
|
||
}
|
||
}
|
||
|
||
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();
|
||
});
|
||
}
|
||
|
||
for (const [width, height, mode] of [
|
||
[390, 844, "embedded"], [1280, 900, "embedded"], [1440, 1000, "full"],
|
||
[844, 390, "embedded"], [320, 640, "embedded"],
|
||
] as const) {
|
||
test(`session confirmation layout stays within ${mode} view at ${width}x${height}`, async ({ page }, testInfo) => {
|
||
await page.setViewportSize({ width, height });
|
||
await fixtures(page, mode);
|
||
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("/");
|
||
if (mode === "embedded") await page.addStyleTag({ content: `
|
||
body { margin: 0; padding-top: 60px; }
|
||
#root { margin-left: ${width > 900 ? 160 : 0}px; height: calc(100dvh - 80px); overflow: hidden; }
|
||
` });
|
||
await expect(page.getByRole("textbox", { name: "New question", exact: true })).toBeVisible();
|
||
await page.getByRole("textbox", { name: "New question", exact: true }).fill("Review the cohort");
|
||
await page.getByRole("button", { name: "Send", exact: true }).click();
|
||
await expect(page.getByRole("textbox", { name: "Steering", exact: true })).toBeVisible();
|
||
await page.evaluate(async () => {
|
||
const path = "/src/store/sessionStore.ts";
|
||
const { useSessionStore } = await import(/* @vite-ignore */ path);
|
||
useSessionStore.getState().applyEvent({ type: "ui_request", ui_request: {
|
||
id: "layout-gate", widget: "artifact-gate", title: "Verifica della domanda e dei criteri di inclusione",
|
||
artifact: { kind: "markdown", content: "## Criteri di inclusione\n\n" + "Verificare i pazienti inclusi nella coorte e il periodo di riferimento.\n\n".repeat(100) },
|
||
options: [{ id: "approve", label: "Conferma e procedi alla fase successiva" }], reserved: ["back", "other"],
|
||
} });
|
||
});
|
||
const dialog = page.getByRole("dialog");
|
||
await expect(dialog).toBeVisible();
|
||
await page.keyboard.press("Escape");
|
||
await expect(dialog).toBeVisible();
|
||
const bounds = await dialog.boundingBox();
|
||
const area = await page.getByTestId("app-shell").boundingBox();
|
||
expect(area).not.toBeNull();
|
||
expect(bounds!.x).toBeGreaterThanOrEqual(area!.x);
|
||
expect(bounds!.y).toBeGreaterThanOrEqual(area!.y);
|
||
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(area!.x + area!.width);
|
||
expect(bounds!.y + bounds!.height).toBeLessThanOrEqual(area!.y + area!.height);
|
||
expect(bounds!.width).toBeGreaterThan(Math.min(1000, area!.width - 60));
|
||
expect(bounds!.height).toBeGreaterThan(area!.height * 0.8);
|
||
const confirmations = dialog.getByRole("button", { name: "Conferma e procedi alla fase successiva" });
|
||
await expect(confirmations).toHaveCount(2);
|
||
for (const button of await confirmations.all()) {
|
||
await button.scrollIntoViewIfNeeded();
|
||
await expect(button).toBeInViewport();
|
||
}
|
||
expect(await dialog.evaluate(el => el.scrollWidth <= el.clientWidth)).toBe(true);
|
||
const body = dialog.locator(".thot-session-dialog__body");
|
||
expect(await body.evaluate(el => el.scrollHeight > el.clientHeight)).toBe(true);
|
||
await body.evaluate(el => { el.scrollTop = el.scrollHeight; });
|
||
await page.screenshot({ path: testInfo.outputPath("session-review.png") });
|
||
await page.evaluate(async () => {
|
||
const path = "/src/store/sessionStore.ts";
|
||
const { useSessionStore } = await import(/* @vite-ignore */ path);
|
||
useSessionStore.getState().clearPending();
|
||
});
|
||
await page.getByRole("button", { name: "Stop and save session", exact: true }).click();
|
||
const stop = page.getByRole("dialog", { name: "Stop the session?" });
|
||
await expect(stop.getByRole("button", { name: "Stop & save", exact: true })).toHaveCount(2);
|
||
await expect(stop.getByRole("button", { name: "Cancel", exact: true })).toBeFocused();
|
||
const stopBounds = await stop.boundingBox();
|
||
expect(stopBounds!.width).toBeGreaterThan(Math.min(600, area!.width - 60));
|
||
expect(stopBounds!.y).toBeGreaterThanOrEqual(area!.y);
|
||
expect(stopBounds!.y + stopBounds!.height).toBeLessThanOrEqual(area!.y + area!.height);
|
||
await page.screenshot({ path: testInfo.outputPath("session-stop.png") });
|
||
await stop.getByRole("button", { name: "Cancel", exact: true }).click();
|
||
await expect(stop).toHaveCount(0);
|
||
});
|
||
}
|