fix(ui): unify heading help, table sizing and navigation contracts
Verified with the coordinated workspace changes by devkit full run 2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed). This shared UI pass does not mark the individual module reviews complete.
This commit is contained in:
+153
@@ -0,0 +1,153 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
|
||||
async function expectDashboardTitleHelp(page: Page) {
|
||||
const title = page.getByRole("heading", { level: 1, name: new URL(page.url()).searchParams.get("language") === "de" ? "Übersicht" : "Dashboard", exact: true });
|
||||
await expect(title).toBeVisible();
|
||||
const anchor = title.locator("..");
|
||||
const help = anchor.getByRole("link");
|
||||
await expect(help).toBeVisible();
|
||||
await expect(title.locator("a")).toHaveCount(0);
|
||||
const headingBounds = await title.boundingBox();
|
||||
const helpBounds = await help.boundingBox();
|
||||
expect(helpBounds!.x - headingBounds!.x - headingBounds!.width).toBeGreaterThanOrEqual(4);
|
||||
expect(helpBounds!.x - headingBounds!.x - headingBounds!.width).toBeLessThanOrEqual(8);
|
||||
await expect(page.locator('[data-page-action-slot="help"], .page-action-bar-trailing .documentation-help-link')).toHaveCount(0);
|
||||
}
|
||||
|
||||
async function dashboardFixture(page: Page) {
|
||||
const writes: string[] = [];
|
||||
const errors: string[] = [];
|
||||
page.on("pageerror", error => errors.push(error.message));
|
||||
await page.route(url => url.pathname.startsWith("/api/"), route => {
|
||||
if (route.request().method() !== "GET") writes.push(new URL(route.request().url()).pathname);
|
||||
return route.fulfill({ json: {
|
||||
exists: false, view_id: null, layout_version: 1, revision: 0,
|
||||
placements: [], known_widget_ids: [], updated_at: null
|
||||
} });
|
||||
});
|
||||
return { writes, errors };
|
||||
}
|
||||
|
||||
for (const language of ["en", "de"]) {
|
||||
test(`Dashboard Cancel exits a clean configuration without saving (${language})`, async ({ page }) => {
|
||||
const fixture = await dashboardFixture(page);
|
||||
await page.goto(`/?dashboard-configuration&language=${language}`);
|
||||
await expectDashboardTitleHelp(page);
|
||||
const configure = page.locator('[data-page-action-archetype="overview"] [data-page-action-slot="primary"] button');
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
await expect(configure).toBeEnabled();
|
||||
await configure.click();
|
||||
await expectDashboardTitleHelp(page);
|
||||
const editor = page.locator('[data-page-action-archetype="editor"]');
|
||||
await expect(editor.locator('[data-page-action-slot="save"] button')).toBeDisabled();
|
||||
const cancel = editor.locator('[data-page-action-slot="discard"] button');
|
||||
await expect(cancel).toBeEnabled();
|
||||
await expect(cancel).toHaveText(language === "de" ? "Abbrechen" : "Cancel");
|
||||
await cancel.click();
|
||||
await expect(configure).toBeVisible();
|
||||
await expectDashboardTitleHelp(page);
|
||||
await expect(page.getByRole("alertdialog")).toHaveCount(0);
|
||||
}
|
||||
expect(fixture.writes).toEqual([]);
|
||||
expect(fixture.errors).toEqual([]);
|
||||
});
|
||||
}
|
||||
|
||||
test("Dashboard Cancel confirms dirty drafts and never saves when discarding", async ({ page }) => {
|
||||
const fixture = await dashboardFixture(page);
|
||||
await page.goto("/?dashboard-configuration&language=en");
|
||||
const configure = page.locator('[data-page-action-archetype="overview"] [data-page-action-slot="primary"] button');
|
||||
await configure.click();
|
||||
await page.getByRole("button", { name: "Remove Active interface modules", exact: true }).click();
|
||||
await expectDashboardTitleHelp(page);
|
||||
const editor = page.locator('[data-page-action-archetype="editor"]');
|
||||
await expect(editor.locator('[data-page-action-slot="save"] button')).toBeEnabled();
|
||||
const cancel = editor.locator('[data-page-action-slot="discard"] button');
|
||||
await cancel.click();
|
||||
const confirmation = page.getByRole("alertdialog");
|
||||
await expect(confirmation).toBeVisible();
|
||||
await confirmation.getByRole("button", { name: "Cancel", exact: true }).click();
|
||||
await expect(editor).toBeVisible();
|
||||
await expect(editor.locator('[data-page-action-slot="save"] button')).toBeEnabled();
|
||||
await cancel.click();
|
||||
await confirmation.getByRole("button", { name: "Discard", exact: true }).click();
|
||||
await expect(configure).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Active interface modules", exact: true })).toBeVisible();
|
||||
await configure.click();
|
||||
await expect(editor.locator('[data-page-action-slot="save"] button')).toBeDisabled();
|
||||
await expect(cancel).toBeEnabled();
|
||||
expect(fixture.writes).toEqual([]);
|
||||
expect(fixture.errors).toEqual([]);
|
||||
});
|
||||
|
||||
test("Dashboard Cancel can save and leave without restoring the previous draft", async ({ page }) => {
|
||||
const fixture = await dashboardFixture(page);
|
||||
let releaseSave!: () => void;
|
||||
const savePending = new Promise<void>(resolve => { releaseSave = resolve; });
|
||||
let saved: Record<string, unknown> | null = null;
|
||||
const updates: Record<string, unknown>[] = [];
|
||||
await page.route("**/api/v1/dashboard/layout*", async route => {
|
||||
if (route.request().method() === "PUT") {
|
||||
const payload = route.request().postDataJSON();
|
||||
updates.push(payload);
|
||||
await savePending;
|
||||
saved = { ...payload, exists: true, view_id: null, revision: 1, updated_at: "2026-09-08T12:00:00Z" };
|
||||
await route.fulfill({ json: saved });
|
||||
} else if (saved) {
|
||||
await route.fulfill({ json: saved });
|
||||
} else {
|
||||
await route.fallback();
|
||||
}
|
||||
});
|
||||
await page.goto("/?dashboard-configuration&language=en");
|
||||
const configure = page.locator('[data-page-action-archetype="overview"] [data-page-action-slot="primary"] button');
|
||||
const editor = page.locator('[data-page-action-archetype="editor"]');
|
||||
await configure.click();
|
||||
await page.getByRole("button", { name: "Remove Active interface modules", exact: true }).click();
|
||||
await editor.locator('[data-page-action-slot="discard"] button').click();
|
||||
const confirmation = page.getByRole("alertdialog");
|
||||
await confirmation.getByRole("button", { name: "Save and leave", exact: true }).click();
|
||||
await expect.poll(() => updates.length).toBe(1);
|
||||
await expect(editor.locator('[data-page-action-slot="discard"] button')).toBeDisabled();
|
||||
await expect(editor.locator('[data-page-action-slot="save"] button')).toBeDisabled();
|
||||
await expect(confirmation.getByRole("button", { name: "Discard", exact: true })).toBeDisabled();
|
||||
releaseSave();
|
||||
await expect(configure).toBeVisible();
|
||||
await expect(confirmation).toHaveCount(0);
|
||||
expect(updates[0].placements).toEqual([]);
|
||||
await page.reload();
|
||||
await configure.click();
|
||||
await expect(page.getByRole("button", { name: "Remove Active interface modules", exact: true })).toHaveCount(0);
|
||||
await expect(editor.locator('[data-page-action-slot="save"] button')).toBeDisabled();
|
||||
await expect(editor.locator('[data-page-action-slot="discard"] button')).toBeEnabled();
|
||||
expect(updates).toHaveLength(1);
|
||||
expect(fixture.writes).toEqual([]);
|
||||
expect(fixture.errors).toEqual([]);
|
||||
});
|
||||
|
||||
test("actual Scheduling widget contributes help at its Dashboard title without duplicating it", async ({ page }) => {
|
||||
const fixture = await dashboardFixture(page);
|
||||
await page.route("**/api/v1/dashboard/layout*", route => route.fulfill({ json: {
|
||||
exists: true, view_id: null, layout_version: 1, revision: 1,
|
||||
placements: [{ instance_id: "scheduling-widget", widget_id: "scheduling.open-requests", size: "medium", column_start: 1, configuration: {} }],
|
||||
known_widget_ids: ["dashboard.installed-modules", "scheduling.open-requests"], updated_at: "2026-09-08T12:00:00Z"
|
||||
} }));
|
||||
await page.route("**/api/v1/scheduling/requests*", route => route.fulfill({ json: { requests: [] } }));
|
||||
await page.goto("/?dashboard-configuration&widget-help&language=en");
|
||||
await expectDashboardTitleHelp(page);
|
||||
const widget = page.locator(".dashboard-widget").filter({ has: page.getByRole("heading", { level: 2, name: "Scheduling requests", exact: true }) });
|
||||
await expect(widget).toHaveCount(1);
|
||||
await expect(widget.getByRole("heading", { name: "Scheduling requests", exact: true })).toHaveCount(1);
|
||||
const titleHelp = widget.locator(".card-title-with-help .documentation-help-link");
|
||||
await expect(titleHelp).toHaveAttribute("href", /topic=scheduling\.find-and-decide-meeting-time/);
|
||||
await expect(titleHelp).toBeVisible();
|
||||
await expect(widget.locator(".card-body .documentation-help-link")).toHaveCount(0);
|
||||
await widget.getByRole("button", { name: "Show header only", exact: true }).click();
|
||||
await expect(titleHelp).toBeVisible();
|
||||
await page.locator('[data-page-action-archetype="overview"] [data-page-action-slot="primary"] button').click();
|
||||
await expectDashboardTitleHelp(page);
|
||||
await expect(titleHelp).toBeVisible();
|
||||
await expect(widget.locator(".card-actions .documentation-help-link")).toHaveCount(0);
|
||||
expect(fixture.writes).toEqual([]);
|
||||
expect(fixture.errors).toEqual([]);
|
||||
});
|
||||
Reference in New Issue
Block a user