feat: consolidate shared UI and harden browser authority for release

This commit is contained in:
2026-09-08 01:35:05 +02:00
parent ac40774785
commit b75ca34295
143 changed files with 8664 additions and 752 deletions
@@ -0,0 +1,100 @@
import { expect, test, type Page } from "@playwright/test";
async function mockAddresses(page: Page) {
const errors: string[] = [];
const writes: string[] = [];
page.on("pageerror", (error) => errors.push(error.message));
const timestamp = "2026-09-01T12:00:00Z";
await page.route(url => url.pathname.startsWith("/api/"), async route => {
const request = route.request();
const path = new URL(request.url()).pathname;
if (request.method() !== "GET") writes.push(path);
let body: unknown = {};
if (path === "/api/v1/addresses/address-books") body = { address_books: [{
id: "book-fixture", name: "Fixture address book", scope_type: "tenant",
source_kind: "local", read_only: false, contact_count: 0, created_at: timestamp, updated_at: timestamp
}] };
else if (path === "/api/v1/addresses/address-lists") body = { address_lists: [{
id: "list-fixture", address_book_id: "book-fixture", name: "Fixture address list",
source_kind: "local", read_only: false, entry_count: 0, created_at: timestamp, updated_at: timestamp
}] };
else if (path === "/api/v1/addresses/sync-sources") body = { sync_sources: [] };
else if (path === "/api/v1/addresses/contacts") body = { contacts: [], total: 0, limit: 50, offset: 0 };
else if (path.endsWith("/entries")) body = { entries: [] };
return route.fulfill({ json: body });
});
return { errors, writes };
}
test("Addressbook keeps Reload/New together and moves specialist actions out of the narrow tree header", async ({ page }) => {
const fixture = await mockAddresses(page);
await page.setViewportSize({ width: 1360, height: 900 });
await page.goto("/?address-explorer&language=en&theme=light");
const toolbar = page.getByRole("toolbar", { name: "Address book actions", exact: true });
const reload = toolbar.locator('[data-page-action-slot="reload"] button');
const create = toolbar.getByRole("button", { name: "Add address book", exact: true });
await expect(reload).toBeEnabled();
await expect(create).toBeVisible();
const [reloadBox, createBox] = await Promise.all([reload.boundingBox(), create.boundingBox()]);
expect(createBox!.x).toBeGreaterThan(1100);
expect(Math.abs(createBox!.y - reloadBox!.y)).toBeLessThan(3);
expect(createBox!.x - reloadBox!.x - reloadBox!.width).toBeLessThan(20);
await expect(page.locator('.address-tree-header button')).toHaveCount(1);
await toolbar.getByRole("button", { name: "Import / export", exact: true }).click();
const transfer = page.getByRole("dialog", { name: "Import / export", exact: true });
await expect(transfer.getByRole("button", { name: "Import contacts", exact: true })).toBeVisible();
await expect(transfer.getByRole("button", { name: "Export address book", exact: true })).toBeVisible();
await expect(transfer.getByRole("combobox", { name: "vCard export version" })).toBeVisible();
await page.keyboard.press("Escape");
await toolbar.getByRole("button", { name: "Connections", exact: true }).click();
const connections = page.getByRole("dialog", { name: "Connections", exact: true });
await expect(connections.getByRole("button", { name: "Connect CardDAV", exact: true })).toBeVisible();
await expect(connections.getByRole("button", { name: "Connect LDAP or Active Directory", exact: true })).toBeVisible();
await expect(connections.getByRole("button", { name: "Run sync", exact: true })).toBeDisabled();
expect(fixture.errors).toEqual([]);
expect(fixture.writes).toEqual([]);
});
test("Addressbook labels select only; folder buttons toggle, including after Reload", async ({ page }) => {
const fixture = await mockAddresses(page);
await page.goto("/?address-explorer&language=en&theme=light");
const root = page.locator('.address-tree-list > .explorer-tree-children > div').first();
const label = root.locator(':scope > .explorer-tree-node-wrap > .explorer-tree-node');
const folder = root.locator(':scope > .explorer-tree-node-wrap > .explorer-tree-toggle');
await expect(folder).toHaveAttribute("aria-expanded", "true");
await label.click();
await expect(label).toHaveAttribute("aria-current", "true");
await expect(folder).toHaveAttribute("aria-expanded", "true");
await expect(page.getByText("Select an address book in this group.", { exact: false })).toBeVisible();
await folder.click();
await expect(folder).toHaveAttribute("aria-expanded", "false");
await label.click();
await expect(folder).toHaveAttribute("aria-expanded", "false");
await page.locator('[data-page-action-slot="reload"] button').click();
await expect(page.locator('[data-page-action-slot="reload"] button')).toBeEnabled();
await expect(folder).toHaveAttribute("aria-expanded", "false");
await expect(label).toHaveAttribute("aria-current", "true");
await folder.click();
const book = page.locator('.address-tree-list .explorer-tree-node').filter({ hasText: "Fixture address book" });
await book.click();
await expect(book).toHaveAttribute("aria-current", "true");
await page.locator('.address-tree-header').getByRole("button", { name: "Manage", exact: true }).click();
const manage = page.getByRole("dialog", { name: "Manage selected book or list", exact: true });
await expect(manage.getByRole("button", { name: "Add address list", exact: true })).toBeVisible();
await expect(manage.getByRole("button", { name: "Edit address book", exact: true })).toBeVisible();
await expect(manage.locator('.form-section-separated').getByRole("button", { name: "Delete address book", exact: true })).toBeVisible();
expect(fixture.errors).toEqual([]);
expect(fixture.writes).toEqual([]);
});
test("Addressbook specialist controls retain permission blockers in German", async ({ page }) => {
const fixture = await mockAddresses(page);
await page.goto("/?address-explorer&language=de&read-only&theme=light");
await expect(page.getByRole("button", { name: "Adressbuch hinzufügen", exact: true })).toBeDisabled();
await page.getByRole("button", { name: "Import / Export", exact: true }).click();
const dialog = page.getByRole("dialog", { name: "Import / Export", exact: true });
await expect(dialog.getByRole("button", { name: "Kontakte importieren", exact: true })).toBeDisabled();
await expect(dialog.getByRole("heading", { name: "In das ausgewählte Adressbuch importieren", exact: true })).toBeVisible();
expect(fixture.errors).toEqual([]);
expect(fixture.writes).toEqual([]);
});