feat(ui): redesign grouped application shell

This commit is contained in:
cesnimda
2026-09-28 17:26:02 +02:00
parent 57a6bd02c6
commit 8e56ceabf5
30 changed files with 398 additions and 92 deletions
+1
View File
@@ -30,6 +30,7 @@ test("public entry points meet automated WCAG A and AA checks", async ({ page })
});
test("primary authenticated workspaces meet automated WCAG A and AA checks", async ({ page }) => {
test.setTimeout(60_000); // Six complete axe scans exceed Playwright's default 30s on slower hosts.
await login(page);
for (const route of ["/dashboard", "/jobs", "/kanban", "/career", "/settings", "/admin/system"]) {
+1 -5
View File
@@ -5,11 +5,7 @@ const apiUrl = "http://localhost:5302/api";
test("CSV export has safe cells and headers, and browser download preserves its UTF-8 bytes", async ({ page }) => {
await page.addInitScript(() => window.localStorage.setItem("uiLanguage", "en"));
await page.goto("/login");
await page.getByLabel("Username or email", { exact: true }).fill("e2e@example.test");
await page.getByLabel("Current password", { exact: true }).fill("E2ePassword123!");
await page.getByRole("button", { name: "Sign in", exact: true }).click();
await expect(page).toHaveURL(/\/dashboard$/);
expect((await page.request.post(`${apiUrl}/auth/login`, { data: { email: "e2e@example.test", password: "E2ePassword123!" } })).status()).toBe(200);
const me = await (await page.request.get(`${apiUrl}/auth/me`)).json();
const csrf = (await page.context().cookies()).find(cookie => cookie.name === "XSRF-TOKEN")!.value;
const headers = { "X-CSRF-TOKEN": csrf, "X-Session-Context": me.sessionContext };
@@ -38,6 +38,7 @@ const test = base.extend<{ headers: Record<string, string> }>({
});
test("[PRES-C01 PRES-C06 PRES-C07 PRES-C10] mobile CV override persists without changing the canonical profile or sibling CV", async ({ page, headers }) => {
test.setTimeout(60_000); // Real API setup, mobile editing, preview and reload share this journey.
const create = async (name: string) => {
const response = await page.request.post(`${apiUrl}/cv/variants`, { headers, data: { name } });
expect(response.status()).toBe(200);
@@ -309,6 +309,46 @@ for (const mode of ["light", "dark"] as const) for (const language of ["en", "nb
}
});
test(`[P5-01 PRES-E07 E08 A03] ${suffix} grouped shell, rail menu and route focus`, async ({ page }, info) => {
test.setTimeout(90_000);
const t = translations[language];
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto("/kanban");
const navigation = page.getByRole("navigation", { name: t.navigation });
await expect(navigation.getByText(t.jobSearch, { exact: true })).toBeVisible();
await expect(navigation.getByText(t.career, { exact: true })).toBeVisible();
await expect(navigation.getByRole("button", { name: t.jobApplications, exact: true })).toHaveAttribute("aria-current", "location");
await expect(navigation.getByRole("button", { name: t.kanbanBoard, exact: true })).toHaveAttribute("aria-current", "page");
await expect(navigation.getByRole("button", { name: t.connectedAccounts, exact: true })).toBeInViewport({ ratio: 1 });
await expect(page.getByRole("link", { name: t.skipToMainContent, exact: true })).toHaveAttribute("href", "#app-main-content");
await capture(page, info, `grouped-shell-${suffix}`);
await scan(page, info, `grouped-shell-${suffix}`, "nav");
await page.getByRole("button", { name: t.collapseSidebar, exact: true }).click();
const jobsRail = page.getByRole("button", { name: t.jobApplications, exact: true });
await jobsRail.focus();
await jobsRail.press("Enter");
const railMenu = page.getByRole("menu");
await expect(railMenu.getByRole("menuitem", { name: t.trash, exact: true })).toBeVisible();
await capture(page, info, `grouped-rail-${suffix}`);
await railMenu.getByRole("menuitem", { name: t.trash, exact: true }).click();
await expect(page).toHaveURL(/\/trash$/);
await expect(page.getByRole("heading", { level: 1, name: t.trash, exact: true })).toBeFocused();
await page.setViewportSize({ width: 390, height: 844 });
const trigger = page.getByRole("button", { name: t.openNavigation, exact: true });
await trigger.click();
const drawer = page.getByRole("dialog", { name: t.navigation });
await expect(drawer.getByText(t.jobSearch, { exact: true })).toBeVisible();
await expect(drawer.getByRole("button", { name: t.trash, exact: true })).toHaveAttribute("aria-current", "page");
await capture(page, info, `grouped-mobile-${suffix}`);
await scan(page, info, `grouped-mobile-${suffix}`, '[role="dialog"]');
await drawer.getByRole("button", { name: t.kanbanBoard, exact: true }).click();
await expect(page).toHaveURL(/\/kanban$/);
await expect(page.getByRole("heading", { level: 1, name: t.kanbanBoard, exact: true })).toBeFocused();
await expect(drawer).not.toBeVisible();
});
if ((mode === "light" && language === "en") || (mode === "dark" && language === "nb")) {
test(`[P5-00 PRES-E07 E09 C06 S02 S03] ${suffix} dirty editor survives shell utilities and cancel`, async ({ page, context, owner }, info) => {
test.setTimeout(90_000);
+10 -5
View File
@@ -47,6 +47,10 @@ async function login(page: Page) {
await loginAs(page, "e2e@example.test", "E2ePassword123!");
}
async function loginForApiSetup(page: Page) {
expect((await page.request.post(`${apiUrl}/auth/login`, { data: { email: "e2e@example.test", password: "E2ePassword123!" } })).status()).toBe(200);
}
async function csrfHeader(page: Page) {
const cookie = (await page.context().cookies()).find((value) => value.name === "XSRF-TOKEN");
expect(cookie, "login should issue a CSRF cookie").toBeTruthy();
@@ -126,7 +130,7 @@ test("a Free account keeps manual work available while AI actions stay honestly
const email = `free-${suffix}@example.test`;
const password = "FreeUserPassword123!";
await login(page);
await loginForApiSetup(page);
const headers = await csrfHeader(page);
const createUser = await page.request.post(`${apiUrl}/users`, {
headers,
@@ -189,11 +193,12 @@ test("a saved job can be created through the reviewed UI flow", async ({ page })
});
test("the dedicated application workspace survives deep links, long data and unsaved navigation", async ({ page }) => {
test.setTimeout(60_000); // API setup and the complete loaded workspace journey exceed 30s on slower hosts.
const suffix = Date.now().toString();
const companyName = `Application Workspace Company With A Deliberately Long Name ${suffix}`;
const title = `Principal Platform Reliability Engineer For Distributed Customer Systems ${suffix}`;
await login(page);
await loginForApiSetup(page);
const headers = await csrfHeader(page);
let companyResponse = await page.request.post(`${apiUrl}/companies`, {
headers,
@@ -243,7 +248,7 @@ test("the dedicated application workspace survives deep links, long data and uns
const job = await jobResponse.json();
await page.goto("/jobs");
const applicationRow = page.getByRole("row", { name: new RegExp(`Open ${title}`, "i") });
const applicationRow = page.locator(`tr[data-job-row-id="${job.id}"]`);
await applicationRow.focus();
await page.keyboard.press("Enter");
await expect(page).toHaveURL(new RegExp(`/jobs/${job.id}$`));
@@ -294,7 +299,7 @@ test("the dedicated application workspace survives deep links, long data and uns
await page.getByRole("button", { name: "Back to applications" }).click();
await expect(page).toHaveURL(/\/jobs$/);
await expect(page.getByRole("row", { name: new RegExp(`Open ${title}`, "i") })).toBeFocused();
await expect(applicationRow).toBeFocused();
await page.goto("/jobs/2147483647");
const missingJobAlert = page.getByRole("alert").filter({ hasText: /Not Found|Could not open this application/i });
@@ -387,7 +392,7 @@ test("Career Workspace loads from the authenticated application shell", async ({
});
test("a public CV renders anonymously and downloads as PDF", async ({ page }, testInfo) => {
await login(page);
await loginForApiSetup(page);
const headers = await csrfHeader(page);
const create = await page.request.post(`${apiUrl}/cv/variants`, {
headers,