Remove the redundant timing-dependent native Argon2 concurrency test while retaining native vector coverage and deterministic limiter coverage. Refresh stale deployment and browser contracts, make release scripts portable across Bash/macOS, and update production dependency locks for resolved security advisories.
835 lines
32 KiB
TypeScript
835 lines
32 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);
|
|
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 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: /Catalog status/ })).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: "View AI description generation logs", 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(72);
|
|
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 + 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,
|
|
);
|
|
}
|
|
});
|