Files
ThothII/frontend/e2e/database-management-layout.spec.ts
T

838 lines
33 KiB
TypeScript

import { expect, test, type Locator, 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<ReturnType<typeof createAuthenticationStack>>;
const databaseId = "11111111-1111-4111-8111-111111111111";
const database = {
id: databaseId,
workspaceId: "psd-clinical",
workspaceName: "Policlinico San Donato",
workspaceAvailable: true,
workspaceRevision: { commit: "a".repeat(40), blob: "b".repeat(40) },
workspaceEvidence: { sourceType: "filesystem", state: "materialized_current_revision" },
runtimeBinding: {
transport: "postgres_direct",
configurationState: "ready",
sessionTransportSupported: 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 unconfiguredDatabase = {
...database,
id: undefined,
workspaceId: "research-lab",
workspaceName: "Research laboratory",
configured: false,
databaseName: "research",
schema: "analytics",
version: 0,
createdAt: "",
updatedAt: "",
binding: { transport: "postgres_direct", port: 5432 },
connectionStatus: "untested",
testedVersion: undefined,
};
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",
};
const descriptionRun = {
id: "77777777-7777-4777-8777-777777777777",
databaseId,
scope: "selected_columns",
modelId: "glm-53",
language: "it",
status: "completed",
total: 5,
processed: 5,
generated: 4,
nonGeneratable: 1,
failed: 0,
inputTokens: 1200,
cacheReadTokens: 80,
outputTokens: 420,
createdAt: "2026-08-31T09:00:00Z",
startedAt: "2026-08-31T09:00:01Z",
updatedAt: "2026-08-31T09:00:04Z",
finishedAt: "2026-08-31T09:00:04Z",
errorSummary: null,
};
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);
const expectedPanelWidth = Math.min(1200, managerBox.width - (managerBox.width <= 768 ? 24 : 32));
expect(Math.abs(panelBox.width - expectedPanelWidth)).toBeLessThanOrEqual(1);
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 expectCurrentNavigation(page: Page, accessibleName: string) {
const navigation = page.getByRole("complementary", { name: "Session navigation" });
const button = navigation.getByRole("button", { name: accessibleName, exact: true });
const available = navigation.locator('[data-navigation-state="available"]').first();
await expect(navigation.locator('[data-navigation-state="current"]')).toHaveCount(1);
await expect(button).toHaveAttribute("data-navigation-state", "current");
await expect(button).toHaveAttribute("aria-current", "page");
expect(await button.getAttribute("class")).toContain("bg-[oklch(var(--nav-active))]");
const [style, availableStyle] = await Promise.all([
button.evaluate((element) => ({
backgroundColor: getComputedStyle(element).backgroundColor,
borderColor: getComputedStyle(element).borderTopColor,
})),
available.evaluate((element) => ({
backgroundColor: getComputedStyle(element).backgroundColor,
borderColor: getComputedStyle(element).borderTopColor,
})),
]);
expect(style.backgroundColor).not.toBe(availableStyle.backgroundColor);
expect(style.borderColor).not.toBe(availableStyle.borderColor);
const tokens = await page.evaluate(() => {
const root = getComputedStyle(document.documentElement);
const read = (name: string) => root.getPropertyValue(name).trim().split(/\s+/).map(Number);
return { primary: read("--primary"), active: read("--nav-active") };
});
expect(tokens.active[2]).toBeCloseTo(tokens.primary[2], 1);
expect(tokens.active[1]).toBeLessThan(tokens.primary[1]);
expect(tokens.active[0]).toBeGreaterThan(tokens.primary[0]);
}
async function expectSelectedSessionScopeTab(page: Page, accessibleName: string) {
const tablist = page.getByRole("tablist", { name: "Session scope" });
const selected = tablist.getByRole("tab", { name: accessibleName, exact: true });
const inactive = tablist.locator('[role="tab"][aria-selected="false"]');
await expect(tablist.locator('[role="tab"][aria-selected="true"]')).toHaveCount(1);
await expect(selected).toHaveAttribute("data-tab-state", "active");
expect(await selected.getAttribute("class")).toContain("bg-[oklch(var(--nav-active))]");
const [selectedStyle, inactiveStyle] = await Promise.all([
selected.evaluate((element) => ({
backgroundColor: getComputedStyle(element).backgroundColor,
borderColor: getComputedStyle(element).borderBottomColor,
})),
inactive.evaluate((element) => ({
backgroundColor: getComputedStyle(element).backgroundColor,
borderColor: getComputedStyle(element).borderBottomColor,
borderWidth: getComputedStyle(element).borderTopWidth,
})),
]);
expect(selectedStyle.backgroundColor).not.toBe(inactiveStyle.backgroundColor);
expect(selectedStyle.borderColor).not.toBe(inactiveStyle.borderColor);
expect(inactiveStyle.borderWidth).toBe("1px");
expect(inactiveStyle.backgroundColor).not.toMatch(/^(?:transparent|rgba\(0, 0, 0, 0\))$/);
expect(inactiveStyle.borderColor).not.toMatch(/^(?:transparent|rgba\(0, 0, 0, 0\))$/);
}
function cssLightness(color: string): number {
const oklab = color.match(/^okl(?:ab|ch)\(([\d.]+)(%)?/);
if (oklab) {
const value = Number(oklab[1]);
return oklab[2] ? value / 100 : value;
}
const channels = color.match(/[\d.]+/g)?.slice(0, 3).map(Number) ?? [];
if (channels.length !== 3) return Number.NaN;
return color.startsWith("color(srgb") ? Math.max(...channels) : Math.max(...channels) / 255;
}
async function expectFleetRowTooltipBelow(trigger: Locator, expectedContent: string) {
await trigger.hover();
await expect.poll(() => trigger.evaluate((element) => {
const owner = element.closest<HTMLElement>("[data-tooltip]");
return owner ? getComputedStyle(owner, "::after").visibility : "missing";
})).toBe("visible");
await expect.poll(() => trigger.evaluate((element) => {
const owner = element.closest<HTMLElement>("[data-tooltip]");
return owner ? getComputedStyle(owner, "::after").opacity : "missing";
})).toBe("1");
const state = await trigger.evaluate((element) => {
const owner = element.closest<HTMLElement>("[data-tooltip]");
if (!owner) throw new Error("Row action tooltip owner is missing");
const style = getComputedStyle(owner, "::after");
return {
content: style.content.replace(/^['\"]|['\"]$/g, ""),
ownerHeight: owner.getBoundingClientRect().height,
top: Number.parseFloat(style.top),
right: Number.parseFloat(style.right),
opacity: style.opacity,
pointerEvents: style.pointerEvents,
backgroundColor: style.backgroundColor,
color: style.color,
};
});
expect(state.content).toBe(expectedContent);
expect(state.top - state.ownerHeight).toBeCloseTo(3, 1);
expect(state.right).toBe(0);
expect(state.opacity).toBe("1");
expect(state.pointerEvents).toBe("none");
expect(cssLightness(state.backgroundColor)).toBeLessThan(0.35);
expect(cssLightness(state.color)).toBeGreaterThan(0.9);
}
async function expectContextPanelHeaderUsesPrimary(
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"]');
await expect(header).toHaveCount(1);
const currentNavigation = page.locator('[data-navigation-state="current"]');
const [headerStyle, navigationStyle] = await Promise.all([
header.evaluate((element) => ({
backgroundColor: getComputedStyle(element).backgroundColor,
backgroundImage: getComputedStyle(element).backgroundImage,
})),
currentNavigation.evaluate((element) => ({
backgroundColor: getComputedStyle(element).backgroundColor,
})),
]);
expect(headerStyle.backgroundImage).toBe("none");
expect(headerStyle.backgroundColor).not.toBe("rgba(0, 0, 0, 0)");
expect(headerStyle.backgroundColor).not.toBe(navigationStyle.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);
const expectedPanelWidth = Math.min(1200, workAreaBox.width - (workAreaBox.width <= 768 ? 24 : 32));
expect(Math.abs(panelBox.width - expectedPanelWidth)).toBeLessThanOrEqual(1);
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 expectWorkAreaPanelHeaderUsesPrimary(
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 currentNavigation = page.locator('[data-navigation-state="current"]');
const [headerStyle, navigationStyle] = await Promise.all([
header.evaluate((element) => ({
backgroundColor: getComputedStyle(element).backgroundColor,
backgroundImage: getComputedStyle(element).backgroundImage,
})),
currentNavigation.evaluate((element) => ({
backgroundColor: getComputedStyle(element).backgroundColor,
})),
]);
expect(headerStyle.backgroundImage).toBe("none");
expect(headerStyle.backgroundColor).not.toBe(navigationStyle.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<string, unknown> = {
"GET /api/catalog/databases": [database, unconfiguredDatabase],
"GET /api/catalog/metadata-generation/models": {
models: [
{ id: "deepseek-v4-pro", label: "DeepSeek V4 Pro" },
{ id: "deepseek-v4-flash", label: "DeepSeek V4 Flash" },
{ id: "glm-53", label: "GLM 5.3" },
{ id: "qwen-36", label: "AritmoLab Qwen 3.6 35B A3B" },
],
default: "glm-53",
},
"GET /api/catalog/description-generation-runs?limit=50": [descriptionRun],
[`GET /api/catalog/description-generation-runs/${descriptionRun.id}`]: descriptionRun,
[`GET /api/catalog/description-generation-runs/${descriptionRun.id}/events-list?after=0`]: [{
sequence: 1,
level: "info",
message: "Description generation completed.",
createdAt: "2026-08-31T09:00:04Z",
}],
"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 expectCurrentNavigation(page, "New session");
await expectSelectedSessionScopeTab(page, "My sessions");
await page.getByRole("tab", { name: "All sessions", exact: true }).click();
await expectSelectedSessionScopeTab(page, "All sessions");
await page.getByRole("tab", { name: "My sessions", exact: true }).click();
await expectSelectedSessionScopeTab(page, "My sessions");
const adminNavigationRail = page.getByRole("complementary", { name: "Session navigation" });
const administration = adminNavigationRail.getByRole("button", { name: "Administration", exact: true });
await expect(administration).toHaveCSS("font-family", /Manrope/);
await expect(administration).toHaveAttribute("aria-expanded", "false");
await expect(adminNavigationRail.getByRole("region", { name: "Administration" })).toHaveCount(0);
await administration.click();
await expect(administration).toHaveAttribute("aria-expanded", "true");
const administrationPanel = adminNavigationRail.getByRole("region", { name: "Administration" });
await expect(administrationPanel).toBeVisible();
expect(await administrationPanel.locator(":scope > *").evaluateAll((elements) => elements.map((element) => (
element.getAttribute("role") === "separator" ? "separator" : element.textContent?.trim()
)))).toEqual([
"Database management",
"separator",
"Workspace management",
"Pi management",
]);
await administration.click();
await expect(administration).toHaveAttribute("aria-expanded", "false");
await expect(adminNavigationRail.getByRole("button", { name: "Database management", exact: true })).toHaveCount(0);
await administration.click();
await expect(administration).toHaveAttribute("aria-expanded", "true");
await adminNavigationRail
.getByRole("button", { name: "Database management", exact: true })
.click();
await expectCurrentNavigation(page, "Database management");
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 expect(page.getByRole("button", { name: /Add database/i })).toHaveCount(0);
await expect(page.getByRole("columnheader", { name: /Revision \/ Evidence/ })).toBeVisible();
await expect(page.getByRole("columnheader", { name: /NL→SQL runtime/ })).toBeVisible();
await expect(page.getByRole("columnheader", { name: /Metadata Catalog/ })).toBeVisible();
const metadataModelSelector = page.getByRole("combobox", {
name: "Metadata-generation LLM model",
});
await expect(metadataModelSelector).toHaveCount(1);
await expect(metadataModelSelector).toHaveValue("glm-53");
await expect(metadataModelSelector.locator("option")).toHaveText([
"DeepSeek V4 Pro",
"DeepSeek V4 Flash",
"GLM 5.3",
"AritmoLab Qwen 3.6 35B A3B",
]);
await page
.getByRole("button", { name: "Configure catalog for Research laboratory", exact: true })
.click();
const configurePanel = page.getByRole("dialog", { name: "Configure catalog" });
await expectContextPanelGeometry(page, "Configure catalog");
const configuredWorkspace = configurePanel.getByLabel("Workspace", { exact: true });
await expect(configuredWorkspace).toHaveValue("Research laboratory");
await expect(configuredWorkspace).toHaveAttribute("readonly", "");
await configurePanel.getByRole("button", { name: "Close database panel" }).click();
await page
.getByRole("button", { name: "Edit Policlinico San Donato", exact: true })
.click();
await expectContextPanelGeometry(page, "Edit database");
await expectContextPanelHeaderUsesPrimary(page, "Edit database");
await page
.getByRole("dialog", { name: "Edit database" })
.getByRole("button", { name: "Close database panel" })
.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 });
const databaseTablesAction = page.getByRole("button", {
name: "View tables for Policlinico San Donato",
exact: true,
});
await expectFleetRowTooltipBelow(databaseTablesAction, "Tables");
await databaseTablesAction.click();
await expect(
page.getByRole("region", {
name: "Tables for Policlinico San Donato",
}),
).toBeVisible();
await page
.getByRole("button", { name: "Description history", exact: true })
.click();
await expectContextPanelGeometry(page, "Description generation");
await expectContextPanelHeaderUsesPrimary(page, "Description generation");
const descriptionPanel = page.getByRole("dialog", { name: "Description generation" });
const descriptionBody = descriptionPanel.locator(
':scope > [data-catalog-panel-region="body"]',
);
const progressTable = descriptionPanel.getByRole("table", {
name: "Description generation progress",
});
const eventLog = descriptionPanel.getByRole("log", {
name: "Description generation events",
});
const recentRuns = descriptionPanel.getByRole("region", {
name: "Description generation history",
});
await expect(progressTable.getByRole("columnheader")).toHaveCount(5);
await expect(eventLog).toBeVisible();
await expect(recentRuns).toBeVisible();
const [bodyBox, eventLogBox, recentRunsBox] = await Promise.all([
descriptionBody.boundingBox(),
eventLog.boundingBox(),
recentRuns.boundingBox(),
]);
expect(bodyBox).not.toBeNull();
expect(eventLogBox).not.toBeNull();
expect(recentRunsBox).not.toBeNull();
if (bodyBox && eventLogBox && recentRunsBox) {
const eventBottomGutter = bodyBox.y + bodyBox.height - eventLogBox.y - eventLogBox.height;
const recentBottomGutter = bodyBox.y + bodyBox.height - recentRunsBox.y - recentRunsBox.height;
expect(eventBottomGutter).toBeGreaterThanOrEqual(10);
expect(recentBottomGutter).toBeGreaterThanOrEqual(10);
expect(eventLogBox.x + eventLogBox.width).toBeLessThan(recentRunsBox.x);
}
await page.setViewportSize({ width: 720, height: 800 });
const [compactManagerBox, compactDescriptionPanelBox] = await Promise.all([
page.getByTestId("conversation-column").getByRole("main", {
name: "Database management",
}).boundingBox(),
descriptionPanel.boundingBox(),
]);
expect(compactManagerBox).not.toBeNull();
expect(compactDescriptionPanelBox).not.toBeNull();
if (compactManagerBox && compactDescriptionPanelBox) {
expect(compactManagerBox.width - compactDescriptionPanelBox.width).toBeGreaterThanOrEqual(20);
}
expect(await descriptionBody.evaluate((element) => getComputedStyle(element).display)).not.toBe("grid");
await page.setViewportSize({ width: 1910, height: 911 });
await descriptionPanel.getByRole("button", { name: /close/i }).click();
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.getByRole("combobox", { name: "Metadata-generation LLM model" }),
).toHaveValue("glm-53");
await expect(metadataModelSelector).toHaveCount(1);
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();
const editPatientsAction = page.getByRole("button", {
name: "Edit description for patients",
exact: true,
});
await expectFleetRowTooltipBelow(editPatientsAction, "Edit metadata");
await editPatientsAction.click();
await expectContextPanelGeometry(page, "Review table description");
await expectContextPanelHeaderUsesPrimary(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);
const administration = page.getByRole("button", { name: "Administration", exact: true });
await expect(administration).toHaveAttribute("aria-expanded", "false");
await administration.click();
await expect(administration).toHaveAttribute("aria-expanded", "true");
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 expectCurrentNavigation(page, "Workspace management");
await expectWorkAreaPanelGeometry(page, "Workspace management");
await expectWorkAreaPanelHeaderUsesPrimary(page, "Workspace management");
const piTrigger = page.getByRole("button", { name: "Pi management", exact: true });
await piTrigger.click();
await expectCurrentNavigation(page, "Pi management");
await expect(
page.getByRole("dialog", { name: "Workspace management" }),
).toHaveCount(0);
await expectWorkAreaPanelGeometry(page, "Pi management");
await expectWorkAreaPanelHeaderUsesPrimary(page, "Pi management");
await page
.getByRole("dialog", { name: "Pi management" })
.getByRole("button", { name: "Close Pi management" })
.click();
await expect(piTrigger).toBeFocused();
await expectCurrentNavigation(page, "New session");
}
await page.setViewportSize({ width: 720, height: 800 });
await page.getByRole("button", { name: "Workspace management", exact: true }).click();
await expectCurrentNavigation(page, "Workspace management");
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 + 24)).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,
);
}
});