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.
154 lines
8.4 KiB
TypeScript
Executable File
154 lines
8.4 KiB
TypeScript
Executable File
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([]);
|
|
});
|