import { expect, test, type Page, type Route } from "@playwright/test"; import { createAuthenticationStack } from "./fixtures/auth-stack.mjs"; test.describe.configure({ mode: "serial" }); test.use({ viewport: { width: 1910, height: 911 } }); let stack: Awaited>; const databaseId = "11111111-1111-4111-8111-111111111111"; const database = { id: databaseId, workspaceId: "psd-clinical", workspaceName: "Policlinico San Donato", workspaceAvailable: true, configured: true, engine: "postgres", databaseName: "warehouse", schema: "datawarehouse", version: 3, createdAt: "2026-08-27T08:00:00Z", updatedAt: "2026-08-27T09:00:00Z", binding: { transport: "postgres_direct", host: "db.internal", port: 5432, username: "reader", }, connectionStatus: "reachable", testedVersion: 3, secrets: { password: false, apiKey: false, sshPrivateKey: false, sshPrivateKeyPassphrase: false, sshKnownHosts: false, tlsCa: false, }, }; const table = { id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", databaseId, name: "patients", sourceComment: "Patients imported from the clinical source", description: null, generatedDescription: null, version: 1, createdAt: "2026-08-27T08:00:00Z", updatedAt: "2026-08-27T09:00:00Z", }; function metrics(requestedDatabaseId: string | null) { return { scope: requestedDatabaseId ? "database" : "global", databaseId: requestedDatabaseId, tables: 1, columns: 0, sensitiveColumns: 0, relationships: 0, descriptionTargets: 1, describedTargets: 0, descriptionCoverage: 0, updatedAt: "2026-08-31T09:00:00Z", }; } function json(route: Route, body: unknown, status = 200) { return route.fulfill({ status, contentType: "application/json", body: JSON.stringify(body), }); } async function signInAsAdmin(page: Page) { const account = stack.localAccount("admin"); await page.getByLabel("Username").fill(account.username); await page.locator('input[name="password"]').fill(account.password); await page.getByRole("button", { name: "Sign in", exact: true }).click(); await expect(page.getByTestId("app-shell")).toBeVisible({ timeout: 30_000 }); } async function expectContextPanelGeometry(page: Page, accessibleName: string) { const manager = page .getByTestId("conversation-column") .getByRole("main", { name: "Database management" }); const panel = manager.getByRole("dialog", { name: accessibleName }); const sessionRail = page.getByRole("complementary", { name: "Session navigation" }); await expect(panel).toBeVisible(); expect( await panel.evaluate((element) => element.hasAttribute("data-catalog-panel")), ).toBe(true); await expect( panel.locator(':scope > [data-catalog-panel-region="header"]'), ).toHaveCount(1); await expect( panel.locator(':scope > [data-catalog-panel-region="body"]'), ).toHaveCount(1); const [managerBox, panelBox, sessionRailBox] = await Promise.all([ manager.boundingBox(), panel.boundingBox(), sessionRail.boundingBox(), ]); expect(managerBox).not.toBeNull(); expect(panelBox).not.toBeNull(); expect(sessionRailBox).not.toBeNull(); if (!managerBox || !panelBox || !sessionRailBox) return; const managerCenter = managerBox.x + managerBox.width / 2; const panelCenter = panelBox.x + panelBox.width / 2; expect(Math.abs(panelCenter - managerCenter)).toBeLessThanOrEqual(1); expect(Math.abs(panelBox.width / managerBox.width - 0.6)).toBeLessThanOrEqual(0.005); expect(panelBox.x).toBeGreaterThanOrEqual(managerBox.x - 1); expect(panelBox.x + panelBox.width).toBeLessThanOrEqual(managerBox.x + managerBox.width + 1); expect(panelBox.x + panelBox.width).toBeLessThanOrEqual(sessionRailBox.x + 1); expect(panelBox.y).toBeGreaterThanOrEqual(managerBox.y); expect(panelBox.y + panelBox.height).toBeLessThanOrEqual(managerBox.y + managerBox.height + 1); } async function expectContextPanelHeaderMatchesNewSession( page: Page, accessibleName: string, ) { const manager = page .getByTestId("conversation-column") .getByRole("main", { name: "Database management" }); const panel = manager.getByRole("dialog", { name: accessibleName }); const header = panel.locator(':scope > [data-catalog-panel-region="header"]'); const newSession = page .getByRole("complementary", { name: "Session navigation" }) .getByRole("button", { name: "New session", exact: true }); await expect(header).toHaveCount(1); await expect(newSession).toBeVisible(); const [headerStyle, actionStyle] = await Promise.all([ header.evaluate((element) => ({ backgroundColor: getComputedStyle(element).backgroundColor, backgroundImage: getComputedStyle(element).backgroundImage, })), newSession.evaluate((element) => ({ backgroundColor: getComputedStyle(element).backgroundColor, })), ]); expect(headerStyle.backgroundImage).toBe("none"); expect(headerStyle.backgroundColor).not.toBe("rgba(0, 0, 0, 0)"); expect(headerStyle.backgroundColor).toBe(actionStyle.backgroundColor); } async function expectWorkAreaPanelGeometry(page: Page, accessibleName: string) { const workArea = page.getByTestId("conversation-column"); const panel = workArea.getByRole("dialog", { name: accessibleName }); const sessionRail = page.getByRole("complementary", { name: "Session navigation" }); await expect(panel).toBeVisible(); await expect(panel).toHaveAttribute("aria-modal", "false"); await expect(panel).toHaveAttribute("data-work-area-panel", ""); await expect(panel.locator(':scope > [data-work-area-panel-region="header"]')).toHaveCount(1); await expect(panel.locator(':scope > [data-work-area-panel-region="body"]')).toHaveCount(1); const [workAreaBox, panelBox, sessionRailBox] = await Promise.all([ workArea.boundingBox(), panel.boundingBox(), sessionRail.boundingBox(), ]); expect(workAreaBox).not.toBeNull(); expect(panelBox).not.toBeNull(); expect(sessionRailBox).not.toBeNull(); if (!workAreaBox || !panelBox || !sessionRailBox) return; expect(Math.abs( panelBox.x + panelBox.width / 2 - (workAreaBox.x + workAreaBox.width / 2), )).toBeLessThanOrEqual(1); expect(Math.abs(panelBox.width / workAreaBox.width - 0.6)).toBeLessThanOrEqual(0.005); expect(panelBox.x).toBeGreaterThanOrEqual(workAreaBox.x - 1); expect(panelBox.x + panelBox.width).toBeLessThanOrEqual(workAreaBox.x + workAreaBox.width + 1); expect(panelBox.x + panelBox.width).toBeLessThanOrEqual(sessionRailBox.x + 1); expect(panelBox.y).toBeGreaterThanOrEqual(workAreaBox.y); expect(panelBox.y + panelBox.height).toBeLessThanOrEqual(workAreaBox.y + workAreaBox.height + 1); } async function expectWorkAreaPanelHeaderMatchesNewSession( page: Page, accessibleName: string, ) { const panel = page .getByTestId("conversation-column") .getByRole("dialog", { name: accessibleName }); const header = panel.locator(':scope > [data-work-area-panel-region="header"]'); const newSession = page .getByRole("complementary", { name: "Session navigation" }) .getByRole("button", { name: "New session", exact: true }); const [headerStyle, actionStyle] = await Promise.all([ header.evaluate((element) => ({ backgroundColor: getComputedStyle(element).backgroundColor, backgroundImage: getComputedStyle(element).backgroundImage, })), newSession.evaluate((element) => ({ backgroundColor: getComputedStyle(element).backgroundColor, })), ]); expect(headerStyle.backgroundImage).toBe("none"); expect(headerStyle.backgroundColor).toBe(actionStyle.backgroundColor); } test.beforeAll(async () => { stack = await createAuthenticationStack(); await stack.useLocalMode(); }); test.afterAll(async () => { await stack?.close(); }); test("context panels stay inside the manager and the Tables grid sits in a sidebar-colored frame", async ({ page }) => { const unexpectedCatalogRequests: string[] = []; const responses: Record = { "GET /api/catalog/databases": [database], "GET /api/catalog/metadata-generation/models": { models: [], default: null, }, "GET /api/catalog/description-generation-runs?limit=50": [], "GET /api/catalog/sensitive-data-suggestion-runs?limit=50": [], [`GET /api/catalog/databases/${databaseId}/tables`]: [table], }; await page.route("**/api/catalog/**", async (route) => { const request = route.request(); const url = new URL(request.url()); const key = `${request.method()} ${url.pathname}${url.search}`; if (request.method() === "GET" && url.pathname === "/api/catalog/metrics") { const requestedId = url.searchParams.get("databaseId"); if (requestedId === null || requestedId === databaseId) { await json(route, metrics(requestedId)); return; } } if (Object.hasOwn(responses, key)) { await json(route, responses[key]); return; } unexpectedCatalogRequests.push(key); await json(route, { code: "unexpected_catalog_request" }, 500); }); await page.goto(stack.publicUrl); await signInAsAdmin(page); await page .getByRole("button", { name: "Database management", exact: true }) .click(); await expect( page.getByRole("complementary", { name: "Session navigation" }), ).toBeVisible(); await expect( page.getByTestId("conversation-column").getByRole("main", { name: "Database management", }), ).toBeVisible(); await expect( page.getByRole("button", { name: "Back to workspace", exact: true }), ).toHaveCount(0); await page .getByRole("button", { name: "Edit Policlinico San Donato", exact: true }) .click(); await expectContextPanelGeometry(page, "Edit database"); await expectContextPanelHeaderMatchesNewSession(page, "Edit database"); await page .getByRole("dialog", { name: "Edit database" }) .getByRole("button", { name: "Close database panel" }) .click(); await page .getByRole("button", { name: "Description history", exact: true }) .click(); await expectContextPanelGeometry(page, "Description generation"); await expectContextPanelHeaderMatchesNewSession(page, "Description generation"); await page .getByRole("dialog", { name: "Description generation" }) .getByRole("button", { name: /close/i }) .click(); await page.setViewportSize({ width: 1280, height: 800 }); await page .getByRole("button", { name: "Edit Policlinico San Donato", exact: true }) .click(); await expectContextPanelGeometry(page, "Edit database"); await page .getByRole("dialog", { name: "Edit database" }) .getByRole("button", { name: "Close database panel" }) .click(); await page.setViewportSize({ width: 1910, height: 911 }); await page .getByRole("button", { name: "View tables for Policlinico San Donato", exact: true, }) .click(); await expect( page.getByRole("region", { name: "Tables for Policlinico San Donato", }), ).toBeVisible(); const applicationBar = page.locator( 'main[aria-label="Database management"] .thot-fleet-ledger__header', ); await expect(applicationBar).toHaveCount(1); await expect( applicationBar.getByRole("heading", { name: "Database management" }), ).toBeVisible(); await expect( applicationBar.getByRole("region", { name: "Catalog status" }), ).toBeVisible(); await expect( applicationBar.getByRole("group", { name: "Database management actions" }), ).toBeVisible(); await expect(applicationBar).not.toContainText("Thoth catalog ยท Fleet ledger"); await expect(applicationBar).not.toContainText("Inspect physical metadata"); const applicationBarLayout = await applicationBar.evaluate((element) => { const title = element.querySelector(".thot-fleet-ledger__heading-copy"); const status = element.querySelector(".thot-fleet-ledger__header-status"); const actions = element.querySelector(".thot-fleet-ledger__header-actions"); if (!title || !status || !actions) { throw new Error("Application bar regions are missing"); } const centerY = (node: Element) => { const rect = node.getBoundingClientRect(); return rect.top + rect.height / 2; }; return { backgroundImage: getComputedStyle(element).backgroundImage, height: element.getBoundingClientRect().height, centers: [centerY(title), centerY(status), centerY(actions)], }; }); expect(applicationBarLayout.backgroundImage).toBe("none"); expect(applicationBarLayout.height).toBeLessThanOrEqual(64); expect( Math.max(...applicationBarLayout.centers) - Math.min(...applicationBarLayout.centers), ).toBeLessThanOrEqual(1); await expect(page.getByRole("row", { name: /patients/ })).toBeVisible(); await page .getByRole("button", { name: "Edit description for patients", exact: true }) .click(); await expectContextPanelGeometry(page, "Review table description"); await expectContextPanelHeaderMatchesNewSession(page, "Review table description"); const tableEditor = page.getByRole("dialog", { name: "Review table description" }); const tableEditorFooter = tableEditor.locator( ':scope > [data-catalog-panel-region="footer"]', ); await expect(tableEditorFooter).toHaveCount(1); await expect( tableEditorFooter.getByRole("button", { name: "Cancel", exact: true }), ).toBeVisible(); await expect( tableEditorFooter.getByRole("button", { name: "Save metadata", exact: true }), ).toBeVisible(); await tableEditor .getByRole("button", { name: "Close table metadata", exact: true }) .click(); const gridRoot = page.locator( 'main[aria-label="Database management"] .thot-fleet-ledger-grid .ag-root-wrapper', ); await expect(gridRoot).toHaveCount(1); await expect(gridRoot).toBeVisible(); await expect.poll( () => gridRoot.evaluate((element) => { const manager = element.closest('main[aria-label="Database management"]'); if (!manager) throw new Error("Database manager is missing"); const managerBottom = manager.getBoundingClientRect().bottom; const gridBottom = element.getBoundingClientRect().bottom; return Math.max( Math.abs(window.innerHeight - managerBottom), Math.abs(managerBottom - gridBottom - 30), ); }), { message: "AG Grid must end 30px above the manager bottom" }, ).toBeLessThanOrEqual(1); const gapColors = await gridRoot.evaluate((element) => { const workspace = element.closest(".thot-fleet-ledger__workspace"); const tableSurface = element.closest(".thot-fleet-ledger__content"); const sidebar = document.querySelector('aside[aria-label="Session navigation"]'); if (!workspace || !tableSurface || !sidebar) { throw new Error("Application layout is missing"); } return { gap: getComputedStyle(workspace).backgroundColor, table: getComputedStyle(tableSurface).backgroundColor, sidebar: getComputedStyle(sidebar).backgroundColor, }; }); expect(gapColors.gap).not.toBe("rgba(0, 0, 0, 0)"); expect(gapColors.gap).toBe(gapColors.sidebar); expect(gapColors.gap).not.toBe(gapColors.table); const workspace = page.locator( 'main[aria-label="Database management"] .thot-fleet-ledger__workspace', ); const tableSurface = page.locator( 'main[aria-label="Database management"] .thot-fleet-ledger__content', ); const sessionNavigation = page.getByRole("complementary", { name: "Session navigation", }); await expect.poll( async () => { const [workspaceBox, tableBox, gridBox, sidebarBox] = await Promise.all([ workspace.boundingBox(), tableSurface.boundingBox(), gridRoot.boundingBox(), sessionNavigation.boundingBox(), ]); if (!workspaceBox || !tableBox || !gridBox || !sidebarBox) { throw new Error("Database frame geometry is missing"); } return Math.max( Math.abs(tableBox.x - workspaceBox.x - 20), Math.abs(tableBox.x + tableBox.width - sidebarBox.x), Math.abs(tableBox.y + tableBox.height - gridBox.y - gridBox.height), ); }, { message: "The table must have a 20px left frame and its right border must stop at the footer", }, ).toBeLessThanOrEqual(1); const frameBorders = await tableSurface.evaluate((element) => { const sidebar = document.querySelector('aside[aria-label="Session navigation"]'); if (!sidebar) throw new Error("Session navigation is missing"); return { tableEnd: Number.parseFloat(getComputedStyle(element).borderInlineEndWidth), sidebarStart: Number.parseFloat( getComputedStyle(sidebar).borderInlineStartWidth, ), }; }); expect(frameBorders.tableEnd).toBe(1); expect(frameBorders.sidebarStart).toBe(0); await expect(page.locator(".thot-session-navigation__boundary")).toHaveCount(0); expect(unexpectedCatalogRequests).toEqual([]); }); test("Workspace and Pi management share the centered work-area panel without covering the session rail", async ({ page }) => { await page.route("**/api/workspace-registry/status", (route) => json(route, { branch: "main", head: "a".repeat(40), ahead: 0, behind: 0, degraded: false, repository: { host: "git.example.test", repository: "analytics/thoth-workspaces", transport: "ssh", }, })); await page.route("**/api/workspaces", (route) => json(route, [])); await page.route("**/api/pi-management/status", (route) => json(route, { version: "0.80.3", ready: true, credentials: "present", config: { provider: "zai", model: "glm-5.2", reasoning: "medium" }, checkedAt: "2026-08-31T09:00:00Z", })); await page.route("**/api/pi-management/options", (route) => json(route, { providers: ["zai"], models: [{ provider: "zai", id: "glm-5.2" }], reasoning: ["low", "medium", "high"], checkedAt: "2026-08-31T09:00:00Z", })); await page.goto(stack.publicUrl); await signInAsAdmin(page); for (const viewport of [ { width: 1910, height: 911 }, { width: 1280, height: 800 }, ]) { await page.setViewportSize(viewport); const workspaceTrigger = page.getByRole("button", { name: "Workspace management", exact: true, }); await workspaceTrigger.click(); await expectWorkAreaPanelGeometry(page, "Workspace management"); await expectWorkAreaPanelHeaderMatchesNewSession(page, "Workspace management"); const piTrigger = page.getByRole("button", { name: "Pi management", exact: true }); await piTrigger.click(); await expect( page.getByRole("dialog", { name: "Workspace management" }), ).toHaveCount(0); await expectWorkAreaPanelGeometry(page, "Pi management"); await expectWorkAreaPanelHeaderMatchesNewSession(page, "Pi management"); await page .getByRole("dialog", { name: "Pi management" }) .getByRole("button", { name: "Close Pi management" }) .click(); await expect(piTrigger).toBeFocused(); } await page.setViewportSize({ width: 720, height: 800 }); await page.getByRole("button", { name: "Workspace management", exact: true }).click(); const compactWorkArea = page.getByTestId("conversation-column"); const compactPanel = compactWorkArea.getByRole("dialog", { name: "Workspace management" }); const [compactWorkAreaBox, compactPanelBox] = await Promise.all([ compactWorkArea.boundingBox(), compactPanel.boundingBox(), ]); expect(compactWorkAreaBox).not.toBeNull(); expect(compactPanelBox).not.toBeNull(); if (compactWorkAreaBox && compactPanelBox) { expect(Math.abs(compactPanelBox.width - compactWorkAreaBox.width + 16)).toBeLessThanOrEqual(1); expect(Math.abs( compactPanelBox.x + compactPanelBox.width / 2 - (compactWorkAreaBox.x + compactWorkAreaBox.width / 2), )).toBeLessThanOrEqual(1); } const compactNavigation = compactPanel.getByRole("navigation", { name: "Workspaces" }); const compactContent = compactPanel.getByRole("region", { name: "Workspace configuration", }); const [compactNavigationBox, compactContentBox] = await Promise.all([ compactNavigation.boundingBox(), compactContent.boundingBox(), ]); expect(compactNavigationBox).not.toBeNull(); expect(compactContentBox).not.toBeNull(); if (compactNavigationBox && compactContentBox) { expect(compactContentBox.y).toBeGreaterThanOrEqual( compactNavigationBox.y + compactNavigationBox.height - 1, ); } });