import { expect, test, type Page, type Request } from "@playwright/test"; import { apiPathPattern, mockApi, seedAuthenticatedState } from "./support/network.js"; import { isDesktopProject } from "./support/projects"; test.use({ serviceWorkers: "block" }); const json = (body: unknown, status = 200) => ({ status, contentType: "application/json", body: JSON.stringify(body), }); const users = [ { id: 11, customer_number: 12345, display_name: "Anna Andersen", group_id: 1, }, { id: 12, customer_number: 67890, display_name: "Bo Nielsen", group_id: 2, }, ]; const userEnvelope = (rows: Array>) => ({ data: rows, meta: { pagination: { page: 1, per_page: 100, total: rows.length, }, }, }); const usersApiPattern = /https?:\/\/(?:api\.truckwash\.io(?::\d+)?\/users|localhost(?::\d+)?\/api\/users|127\.0\.0\.1(?::\d+)?\/api\/users)(?:\?.*)?$/i; const expectNoPageHorizontalOverflow = async (page: Page) => { const metrics = await page.evaluate(() => ({ clientWidth: document.documentElement.clientWidth, scrollWidth: document.documentElement.scrollWidth, })); expect(metrics.scrollWidth).toBeLessThanOrEqual(metrics.clientWidth + 1); }; const expectTileGridLayout = async (page: Page, testId: string, expectMultipleColumns: boolean) => { const grid = page.getByTestId(testId); await expect(grid).toBeVisible(); const columnCount = await grid.evaluate( (element) => window .getComputedStyle(element) .gridTemplateColumns.split(" ") .filter((column) => column && column !== "none").length ); if (expectMultipleColumns) { expect(columnCount).toBeGreaterThan(1); return; } expect(columnCount).toBe(1); }; const expectActiveUserDetailTab = async (page: Page, activeTab: string) => { const tabLabel = page.getByTestId(`superuser-user-tab-${activeTab}`); const tab = page.locator('[role="tab"]').filter({ has: tabLabel }); await expect(tabLabel).toBeVisible(); await expect(tab).toHaveAttribute("aria-selected", "true"); }; const expectUserDetailShell = async ( page: Page, activeTab: string, contentTestId = "superuser-user-detail-content" ) => { await expect(page.getByTestId("superuser-user-detail-shell")).toBeVisible(); await expect(page.getByTestId("superuser-user-tabs")).toBeVisible(); await expect(page.getByTestId(contentTestId)).toBeVisible(); await expect(page.getByTestId("superuser-user-page-header-title")).toHaveCount(1); await expectActiveUserDetailTab(page, activeTab); await expectNoPageHorizontalOverflow(page); }; const overviewUser = { id: 11, customer_number: 12345, display_name: "Anna Andersen", email: "anna@example.test", phone: { number: "12345678", country_code: 45, }, group_id: 1, created_at: "2026-01-01 08:30:00", updated_at: "2026-02-01 09:45:00", economic_customer: { customerNumber: 12345, name: "Anna Transport", address: "Main Road 1", zip: "2100", city: "Copenhagen", mobilePhone: "12345678", email: "billing@example.test", corporateIdentificationNumber: "12345678", currency: "DKK", country: "Denmark", barred: false, }, permissions: ["superuser", "user", "get_user", "set_custom_price"], attributes: [{ id: 1, attribute: "invoiceAllOrdersIndividually" }], discounts: [{ id: 999999, percentage: 10 }], orders_not_invoiced: [{ id: 501 }], keys: { open_invoice_draft: "DRAFT-1", OtherSpecialArrangement: "Night wash agreement", OtherVaskeabonnement: "Monthly wash subscription note", }, wash_subscription_transactions: [ { id: 6250, customer_id: 12345, cashier_id: 1857, reference: "Vaskeabonnementer", notes: "", department_id: 10, reg_1: "", reg_2: "", reg_3: "", completed_at: null, created_at: "2026-03-01 00:00:01", deleted_at: null, total_net_amount: 694, invoice_collection_id: 1634, booking_id: 0, closed_at: "2026-03-24 12:45:10", }, ], }; const overviewVehicles = [ { id: 301, customer_id: 12345, reg: "AA11223", type: 1, reference: "Truck 1", wash_subscription: true, xlvask: true, addons: { list: [{ product: { id: 91, name: "Interior cleaning" } }], }, }, { id: 302, customer_id: 12345, reg: "BB44556", type: 1, reference: "Trailer", wash_subscription: false, xlvask: false, addons: { list: [], }, }, ]; const scopedSubusers = [ { id: 701, username: "driver-one", name: "Driver One", email: "driver-one@example.test", phone_country_code: 45, phone: 70100100, created_at: "2026-03-01 08:00:00", updated_at: "2026-03-02 09:00:00", setup_required: false, can_resend_invite: false, customer_number: 12345, customer_name: "Anna Transport", grant_id: 801, grant_enabled: true, grant_note: "Primary driver", grant_permissions: ["VEHICLES_LIST", "BOOKINGS_LIST"], permissions: ["VEHICLES_LIST", "BOOKINGS_LIST"], access_state: "active", }, { id: 702, username: null, name: "Pending Driver", email: null, phone_country_code: 45, phone: 70200200, created_at: "2026-03-03 08:00:00", updated_at: "2026-03-03 08:00:00", setup_required: true, can_resend_invite: true, customer_number: 12345, customer_name: "Anna Transport", grant_id: 802, grant_enabled: true, grant_note: "", grant_permissions: ["VEHICLES_LIST"], permissions: ["VEHICLES_LIST"], access_state: "pending_setup", }, ]; const scopedSubusersSummary = { total: 2, active: 1, pending_setup: 1, disabled: 0, }; const pricingProducts = [ { id: 101, category: 7, name: "Foam Deluxe", price: 199, is_wash: true, subscription_allowed: true, addons: [], }, ]; const securityPermissionCatalog = { "/orders": { GET: { list_orders: "List orders", }, POST: { add_order: "Create orders", }, }, "/customer/pricing": { POST: { set_custom_price: "Set custom prices", }, }, "/superuser/user": { GET: { get_user: "Get user", }, }, }; const securityDepartments = [ { id: 1, name: "Copenhagen" }, { id: 2, name: "Odense" }, ]; const clickRolePermissionSwitch = async (page: Page, permission: string) => { await page.locator(`label[for="role-permission-${permission}"]`).click(); }; const readRequestJson = (request: Request): Record | null => { try { return request.postDataJSON(); } catch { return null; } }; const setupOverviewApi = async (page, options: { failDetailUntilEnabled?: boolean } = {}) => { let detailRequests = 0; let detailSuccessEnabled = !options.failDetailUntilEnabled; const detailUser = JSON.parse(JSON.stringify(overviewUser)); let lastUserUpdatePayload: Record | null = null; let customerAttributes = Array.isArray(detailUser.attributes) ? [...detailUser.attributes] : []; const customerAttributeCalls: Array<{ action: string; attribute: string }> = []; const rolePermissionCalls: Array<{ action: string; groupId: number; permission: string }> = []; const subuserRequests: Array<{ method: string; pathname: string; body: Record | null }> = []; const vehicleRequests: Array<{ method: string; pathname: string; body: Record | null }> = []; const rolePermissionsById = new Map([ [1, [...overviewUser.permissions, "list_orders"]], [2, ["user", "get_user"]], [3, ["user"]], ]); const roleRows = [ { id: 1, name: "Administrators" }, { id: 2, name: "Customer Support" }, { id: 3, name: "Read only" }, ]; const rolePayload = (roleId: number) => { const role = roleRows.find((row) => row.id === roleId) || { id: roleId, name: `#${roleId}` }; return { ...role, description: `${role.name} role`, permissions: rolePermissionsById.get(roleId) || [], }; }; await page.route(apiPathPattern("/superuser/user"), async (route) => { const request = route.request(); if (request.method() !== "GET") { await route.fulfill(json({ data: { message: "OK" } })); return; } detailRequests += 1; if (!detailSuccessEnabled) { await route.fulfill(json({ message: "User detail unavailable" }, 500)); return; } await route.fulfill(json({ data: detailUser })); }); await page.route(apiPathPattern("/permissions"), async (route) => { await route.fulfill(json({ data: securityPermissionCatalog })); }); await page.route(apiPathPattern("/departments"), async (route) => { await route.fulfill(json({ data: securityDepartments })); }); await page.route(apiPathPattern("/superuser/user/keys"), async (route) => { if (route.request().method() === "POST") { await route.fulfill(json({ data: { message: "OK" } })); return; } await route.fulfill( json({ data: { OtherSpecialArrangement: overviewUser.keys.OtherSpecialArrangement, OtherVaskeabonnement: overviewUser.keys.OtherVaskeabonnement, }, }) ); }); await page.route(apiPathPattern("/customer/pricing/fixed"), async (route) => { await route.fulfill( json({ data: { price: 1234, description: "Fixed agreement", }, }) ); }); await page.route(apiPathPattern("/customer/department/default"), async (route) => { await route.fulfill( json({ data: { department: 1, }, }) ); }); await page.route(apiPathPattern("/vehicles"), async (route) => { const request = route.request(); if (request.method() !== "GET") { await route.fulfill(json({ data: { message: "OK" } })); return; } const url = new URL(request.url()); const filters = url.searchParams.get("filters") || ""; const rows = filters.includes("wash_subscription:1") ? overviewVehicles.filter((vehicle) => vehicle.wash_subscription) : overviewVehicles; await route.fulfill(json({ data: rows })); }); await page.route(apiPathPattern("/products"), async (route) => { await route.fulfill(json({ data: pricingProducts })); }); await page.route(apiPathPattern("/roles"), async (route) => { const url = new URL(route.request().url()); const roleId = Number.parseInt(url.searchParams.get("id") || "", 10); await route.fulfill( json({ data: Number.isFinite(roleId) ? rolePayload(roleId) : roleRows.map((role) => rolePayload(role.id)), }) ); }); await page.route(apiPathPattern("/roles/permissions"), async (route) => { const request = route.request(); const method = request.method(); const url = new URL(request.url()); const body = method === "POST" ? request.postDataJSON() : {}; const groupId = Number.parseInt(String(method === "POST" ? body.group_id : url.searchParams.get("group_id")), 10); const permission = method === "POST" ? String(body.permission_id || "") : String(url.searchParams.get("permission_id") || ""); const action = method === "POST" ? "add" : "remove"; const existing = rolePermissionsById.get(groupId) || []; rolePermissionCalls.push({ action, groupId, permission }); rolePermissionsById.set( groupId, action === "add" ? [...new Set([...existing, permission])] : existing.filter((value) => value !== permission) ); await route.fulfill(json({ data: rolePayload(groupId) })); }); await page.route(apiPathPattern("/customer/attributes"), async (route) => { const request = route.request(); if (request.method() === "POST") { const body = request.postDataJSON(); const attribute = String(body.attribute || ""); customerAttributeCalls.push({ action: "add", attribute }); if (!customerAttributes.some((entry) => String(entry?.attribute || entry) === attribute)) { customerAttributes = [ ...customerAttributes, { id: customerAttributes.length + 1, attribute, }, ]; } detailUser.attributes = [...customerAttributes]; await route.fulfill(json({ data: { message: "OK" } })); return; } if (request.method() === "DELETE") { const url = new URL(request.url()); const attribute = String(url.searchParams.get("attribute") || ""); customerAttributeCalls.push({ action: "remove", attribute }); customerAttributes = customerAttributes.filter((entry) => String(entry?.attribute || entry) !== attribute); detailUser.attributes = [...customerAttributes]; await route.fulfill(json({ data: { message: "OK" } })); return; } await route.fulfill(json({ data: customerAttributes })); }); await page.route(apiPathPattern("/superuser/users/11/vehicles"), async (route) => { const request = route.request(); const url = new URL(request.url()); const method = request.method(); const body = method === "GET" ? null : readRequestJson(request); vehicleRequests.push({ method, pathname: url.pathname, body }); if (method === "GET") { await route.fulfill( json({ data: overviewVehicles, meta: { pagination: { page: 1, per_page: 100, total: overviewVehicles.length, }, user_context: { user_id: 11, customer_number: 12345, customer_name: "Anna Transport", }, }, }) ); return; } if (method === "POST") { await route.fulfill( json({ data: { id: 303, customer_id: 12345, reg: body?.reg || "NEW123", type: Number(body?.type || 1), reference: body?.reference || null, wash_subscription: Boolean(body?.wash_subscription), xlvask: false, addons: { list: [] }, }, }) ); return; } if (method === "PUT" || method === "DELETE") { await route.fulfill(json({ data: { message: "OK" } })); return; } await route.fulfill(json({ data: { message: "Unsupported" } }, 405)); }); await page.route(apiPathPattern("/superuser/users/11/vehicles/summary"), async (route) => { const request = route.request(); const url = new URL(request.url()); const method = request.method(); const body = method === "GET" ? null : readRequestJson(request); vehicleRequests.push({ method, pathname: url.pathname, body }); await route.fulfill( json({ data: { total: 2, wash_subscription: 1, self_service: 1, }, meta: { user_context: { user_id: 11, customer_number: 12345, customer_name: "Anna Transport", }, }, }) ); }); await page.route(apiPathPattern("/superuser/users/11/subusers"), async (route) => { const request = route.request(); const url = new URL(request.url()); const method = request.method(); const body = method === "GET" ? null : readRequestJson(request); subuserRequests.push({ method, pathname: url.pathname, body }); if (method === "GET" && url.pathname.endsWith("/summary")) { await route.fulfill( json({ data: scopedSubusersSummary, meta: { user_context: { user_id: 11, customer_number: 12345 } } }) ); return; } if (method === "GET") { await route.fulfill( json({ data: scopedSubusers, meta: { pagination: { page: 1, per_page: 100, total: scopedSubusers.length, }, user_context: { user_id: 11, customer_number: 12345, customer_name: "Anna Transport", }, subusers_summary: scopedSubusersSummary, }, }) ); return; } if (method === "PATCH") { await route.fulfill(json({ data: { message: "OK" } })); return; } if (method === "POST") { await route.fulfill( json({ data: { subuser: scopedSubusers[1], invite: { setup_link: "https://example.test/subusers/setup?token=test-token", delivery: { status: "sent", }, }, }, }) ); return; } await route.fulfill(json({ data: { message: "Unsupported" } }, 405)); }); await page.route(apiPathPattern("/superuser/users/11/subusers/summary"), async (route) => { const request = route.request(); const url = new URL(request.url()); const method = request.method(); const body = method === "GET" ? null : readRequestJson(request); subuserRequests.push({ method, pathname: url.pathname, body }); await route.fulfill( json({ data: scopedSubusersSummary, meta: { user_context: { user_id: 11, customer_number: 12345 } } }) ); }); await page.route(apiPathPattern("/superuser/users/11/subusers/invite"), async (route) => { const request = route.request(); const url = new URL(request.url()); const method = request.method(); const body = method === "GET" ? null : readRequestJson(request); subuserRequests.push({ method, pathname: url.pathname, body }); await route.fulfill( json({ data: { subuser: scopedSubusers[1], invite: { setup_link: "https://example.test/subusers/setup?token=test-token", delivery: { status: "sent", }, }, }, }) ); }); await page.route(usersApiPattern, async (route) => { const request = route.request(); if (request.method() !== "PUT") { await route.fulfill(json(userEnvelope(users))); return; } lastUserUpdatePayload = await request.postDataJSON(); detailUser.group_id = Number(lastUserUpdatePayload?.role || detailUser.group_id); await route.fulfill(json({ data: { message: "OK" } })); }); await page.route(apiPathPattern("/modules/xlvask/customers"), async (route) => { await route.fulfill( json({ data: [ { customerId: 777, externId: "12345", name: "Anna XLVask", phone: "12345678", email: "xlvask@example.test", country: "Denmark", updated: "2026-03-24 12:45:10", }, ], }) ); }); await page.route(apiPathPattern("/modules/xlvask/vehicles"), async (route) => { await route.fulfill( json({ data: [ { vehicleId: 9001, registrationNumber: "XL11223", customerId: 777, vehicleTypeId: 1, active: true, autoStartOnLpr: true, }, ], }) ); }); return { enableDetailSuccess: () => { detailSuccessEnabled = true; }, customerAttributeCalls: () => customerAttributeCalls, detailRequests: () => detailRequests, detailUser: () => detailUser, lastUserUpdatePayload: () => lastUserUpdatePayload, rolePermissionCalls: () => rolePermissionCalls, subuserRequests: () => subuserRequests, vehicleRequests: () => vehicleRequests, }; }; test.describe("Superuser employees list", () => { test("uses shared search, pagination reload, and action wheel controls", async ({ page }, testInfo) => { test.skip(!isDesktopProject(testInfo), "Desktop only"); const searchesSeen: Array = []; await page.setViewportSize({ width: 1280, height: 720 }); await seedAuthenticatedState(page, "superuser-users-token"); await mockApi(page, { authenticated: true, permissions: ["superuser", "user"], sessionData: { group_id: 1, }, }); await page.route(usersApiPattern, async (route) => { const request = route.request(); const url = new URL(request.url()); if (request.method() !== "GET") { await route.fulfill(json({ data: { message: "OK" } })); return; } const search = url.searchParams.get("search"); searchesSeen.push(search); const normalizedSearch = String(search || "").toLowerCase(); const rows = normalizedSearch.includes("anna") ? [users[0]] : users; await route.fulfill(json(userEnvelope(rows))); }); await page.goto("/superuser/users", { waitUntil: "domcontentloaded" }); await expect(page.getByTestId("superuser-users-shell")).toBeVisible(); await expect(page.getByTestId("superuser-users-index")).toBeVisible(); await expect(page.getByTestId("pagination-reload-actions")).toBeVisible(); await expect(page.getByTestId("pagination-search-input")).toHaveAttribute( "placeholder", /Søg efter bruger|Search for user/ ); await expect(page.getByTestId("superuser-users-table")).toBeVisible(); await expect(page.getByTestId("superuser-users-row-11")).toContainText("Anna Andersen"); await expect(page.getByTestId("superuser-users-row-12")).toContainText("Bo Nielsen"); await expectNoPageHorizontalOverflow(page); await page.getByTestId("pagination-search-input").fill("Anna"); await expect(page.getByTestId("superuser-users-row-11")).toContainText("Anna Andersen"); await expect(page.getByTestId("superuser-users-row-12")).toHaveCount(0); await page.locator('[data-testid="superuser-user-actions-11"] .action-settings-wheel-trigger').click(); await expect(page.getByTestId("superuser-user-edit-11")).toBeVisible(); await expect(page.getByTestId("action-settings-wheel-section-customer")).toBeVisible(); expect(searchesSeen).toContain("Anna"); }); test("migrates an existing employee account into limited backoffice", async ({ page }, testInfo) => { test.skip(!isDesktopProject(testInfo), "Desktop only"); await page.setViewportSize({ width: 1280, height: 720 }); await seedAuthenticatedState(page, "superuser-users-migration-token"); await mockApi(page, { authenticated: true, permissions: ["superuser", "user"], sessionData: { group_id: 1, }, }); const legacyEmployee = { id: 13, customer_number: 0, display_name: "Legacy Clerk", group_id: 2, limited_backoffice_managed: false, }; const managedEmployee = { id: 14, customer_number: 0, display_name: "Managed Clerk", group_id: 77, limited_backoffice_managed: true, }; const customerUser = { id: 15, customer_number: 12345, display_name: "Customer Account", group_id: 2, limited_backoffice_managed: false, }; const rows = [legacyEmployee, managedEmployee, customerUser]; let usersRequests = 0; let migrationPayload: unknown = null; await page.route(usersApiPattern, async (route) => { usersRequests += 1; await route.fulfill(json(userEnvelope(rows))); }); await page.route(apiPathPattern("/limited-backoffice/departments"), async (route) => { await route.fulfill( json({ data: [ { id: 1, name: "Assigned Depot" }, { id: 2, name: "Remote Depot" }, ], }) ); }); await page.route(apiPathPattern("/limited-backoffice/roles"), async (route) => { await route.fulfill( json({ data: [ { key: "viewer", label: "Deactivated" }, { key: "cashier", label: "Cashier" }, { key: "department_admin", label: "Department admin" }, ], }) ); }); await page.route(apiPathPattern("/limited-backoffice/employees/13/migrate"), async (route) => { if (route.request().method() !== "POST") { await route.fulfill(json({ data: { message: "OK" } })); return; } migrationPayload = await route.request().postDataJSON(); await route.fulfill( json({ data: { id: 13, user_id: 13, customer_number: 0, display_name: "Legacy Clerk", role: { key: "cashier", label: "Cashier" }, departments: [{ id: 1, name: "Assigned Depot" }], active: true, }, }) ); }); await page.goto("/superuser/users", { waitUntil: "domcontentloaded" }); await expect(page.getByTestId("superuser-users-row-13")).toContainText("Legacy Clerk"); await page.locator('[data-testid="superuser-user-actions-13"] .action-settings-wheel-trigger').click(); await expect(page.getByTestId("superuser-user-migrate-limited-13")).toBeVisible(); await expect(page.getByTestId("superuser-user-migrate-limited-13")).toContainText( /Migrate to limited backoffice|Migrer til begrænset backoffice/ ); await expect(page.getByTestId("superuser-user-migrate-limited-13")).not.toContainText( "superuser.pages.employees.migrate_to_limited_backoffice" ); await page.getByTestId("superuser-user-migrate-limited-13").click(); await expect(page.locator(".swal2-popup")).toContainText("Migrate employee"); await page.locator(".limited-migration-department").first().check(); await page.locator(".swal2-confirm").click(); await expect.poll(() => migrationPayload).toEqual({ role_key: "cashier", department_ids: [1] }); await expect(page.locator(".swal2-popup")).toContainText("Employee migrated"); await page.locator(".swal2-confirm").click(); await expect.poll(() => usersRequests).toBeGreaterThan(1); await page.locator('[data-testid="superuser-user-actions-14"] .action-settings-wheel-trigger').click(); await expect(page.getByTestId("superuser-user-migrate-limited-14")).toHaveCount(0); await page.locator('[data-testid="superuser-user-actions-15"] .action-settings-wheel-trigger').click(); await expect(page.getByTestId("superuser-user-migrate-limited-15")).toHaveCount(0); }); }); test.describe("Superuser user overview", () => { test.beforeEach(async ({ page }) => { await seedAuthenticatedState(page, "superuser-user-overview-token"); await mockApi(page, { authenticated: true, permissions: [ "superuser", "user", "get_user", "set_custom_price", "list_vehicles_other", "add_vehicle_other", "edit_vehicle_other", "delete_vehicle_other", ], pos: { vehicles: overviewVehicles, }, sessionData: { group_id: 1, }, }); }); test("loads the overview workspace without raw debug output", async ({ page }) => { const overviewApi = await setupOverviewApi(page); overviewApi.detailUser().economic_customer.address = ""; overviewApi.detailUser().economic_customer.zip = null; overviewApi.detailUser().economic_customer.mobilePhone = ""; overviewApi.detailUser().economic_customer.corporateIdentificationNumber = null; overviewApi.detailUser().economic_customer.currency = ""; overviewApi.detailUser().economic_customer.country = null; await page.goto("/superuser/users/11", { waitUntil: "domcontentloaded" }); await expect(page.getByTestId("superuser-user-overview-page")).toBeVisible(); await expectUserDetailShell(page, "overview", "superuser-user-overview-page"); await expect(page.getByTestId("superuser-user-page-header-title")).toContainText("Anna Transport"); await expect(page.getByTestId("superuser-user-tab-overview")).toBeVisible(); await expect(page.getByTestId("superuser-user-tab-pricing")).toBeVisible(); await expect(page.getByTestId("superuser-user-tab-security")).toBeVisible(); await expect(page.getByTestId("superuser-user-overview-metric-customer")).toContainText("12345"); await expect(page.getByTestId("superuser-user-overview-metric-vehicles")).toContainText("2"); await expect(page.getByTestId("superuser-user-overview-metric-orders")).toContainText("1"); await expect(page.getByTestId("superuser-user-overview-metric-discounts")).toContainText("1"); await expect(page.getByTestId("superuser-user-overview-header")).toHaveCount(0); await expect(page.getByTestId("superuser-user-tabs-actions")).toBeVisible(); await expect(page.getByTestId("superuser-user-overview-account")).toContainText("anna@example.test"); await expect(page.getByTestId("superuser-user-overview-economic")).toContainText("billing@example.test"); await expect(page.getByTestId("superuser-user-overview-economic")).not.toContainText( /No data|Ingen data|Keine Daten/i ); await expect(page.getByTestId("superuser-user-overview-customer-actions")).toBeVisible(); await expect(page.getByTestId("superuser-user-overview-customer-shortcuts")).toBeVisible(); await expect(page.getByTestId("superuser-user-overview-customer-flags")).toHaveCount(0); await expect(page.getByTestId("superuser-user-overview-access")).toHaveCount(0); await expect(page.getByTestId("superuser-user-overview-rules")).toHaveCount(0); await expect(page.getByTestId("superuser-user-overview-fixed-pricing")).toHaveCount(0); await expect(page.getByTestId("superuser-user-overview-default-department")).toHaveCount(0); await expect(page.getByTestId("superuser-user-overview-special-arrangement")).toHaveCount(0); await expect(page.getByTestId("superuser-user-overview-wash-subscription-note")).toHaveCount(0); await expect(page.getByTestId("superuser-user-overview-group-id-select")).toHaveCount(0); await expect(page.getByTestId("superuser-user-overview-group-id-label")).toContainText("Administrators"); expect(overviewApi.lastUserUpdatePayload()).toBeNull(); expect(overviewApi.detailUser().group_id).toBe(1); await expect(page.getByTestId("superuser-user-overview-link-vehicles")).toHaveAttribute( "href", "/superuser/users/11/vehicles" ); await expect(page.getByTestId("superuser-user-overview-link-security")).toHaveAttribute( "href", "/superuser/users/11/security" ); await expect(page.getByTestId("superuser-user-overview-page")).not.toContainText("SessionUser:"); const body = page.locator("body"); await expect(body).not.toContainText("SessionUser:"); await expect(body).not.toContainText("economicData:"); }); test("navigates between route-backed user detail pages with the Buefy tabs", async ({ page }) => { await setupOverviewApi(page); await page.goto("/superuser/users/11", { waitUntil: "domcontentloaded" }); await expectUserDetailShell(page, "overview", "superuser-user-overview-page"); const tabRoutes = [ { key: "pricing", path: "/superuser/users/11/pricing", }, { key: "security", path: "/superuser/users/11/security", contentTestId: "superuser-user-security-page", }, { key: "subusers", path: "/superuser/users/11/subusers", contentTestId: "superuser-user-subusers-page", }, { key: "vehicles", path: "/superuser/users/11/vehicles", contentTestId: "superuser-user-vehicles-page", }, { key: "overview", path: "/superuser/users/11", contentTestId: "superuser-user-overview-page", }, ]; for (const tabRoute of tabRoutes) { await page.getByTestId(`superuser-user-tab-${tabRoute.key}`).click(); await expect(page).toHaveURL(new RegExp(`${tabRoute.path}$`)); await expectUserDetailShell(page, tabRoute.key, tabRoute.contentTestId); } }); test("top overview tiles expose hover actions and open the relevant user areas", async ({ page }) => { await setupOverviewApi(page); await page.goto("/superuser/users/11", { waitUntil: "domcontentloaded" }); await page.getByTestId("superuser-user-overview-metric-customer").hover(); await expect(page.getByTestId("superuser-user-overview-metric-customer-action")).toContainText( /View account|Vis konto/ ); await page.getByTestId("superuser-user-overview-metric-economic").hover(); await expect(page.getByTestId("superuser-user-overview-metric-economic-action")).toContainText( /View e-conomic|Vis e-conomic/ ); const tileRoutes = [ { action: /Open vehicles|Åbn køretøjer/, key: "vehicles", path: /\/superuser\/users\/11\/vehicles$/, }, { action: /Open subscriptions|Åbn abonnementer/, key: "subscriptions", path: /\/superuser\/users\/11\/vehicles$/, }, { action: /Open orders|Åbn ordrer/, key: "orders", path: /\/superuser\/users\/11\/orders$/, }, { action: /Open pricing|Åbn priser/, key: "discounts", path: /\/superuser\/users\/11\/pricing$/, }, { action: /Open security|Åbn sikkerhed/, key: "attributes", path: /\/superuser\/users\/11\/security$/, }, { action: /Open security|Åbn sikkerhed/, key: "permissions", path: /\/superuser\/users\/11\/security$/, }, ]; for (const tile of tileRoutes) { await page.goto("/superuser/users/11", { waitUntil: "domcontentloaded" }); await page.getByTestId(`superuser-user-overview-metric-${tile.key}`).hover(); await expect(page.getByTestId(`superuser-user-overview-metric-${tile.key}-action`)).toContainText(tile.action); await page.getByTestId(`superuser-user-overview-metric-${tile.key}`).click(); await expect(page).toHaveURL(tile.path); } }); test("shows a retryable error when the detail request fails", async ({ page }) => { const overviewApi = await setupOverviewApi(page, { failDetailUntilEnabled: true }); await page.goto("/superuser/users/11", { waitUntil: "domcontentloaded" }); await expect(page.getByTestId("superuser-user-overview-error")).toContainText("User detail unavailable"); expect(overviewApi.detailRequests()).toBeGreaterThan(0); overviewApi.enableDetailSuccess(); await page.getByTestId("superuser-user-overview-retry").click(); await expect(page.getByTestId("superuser-user-page-header-title")).toContainText("Anna Transport"); await expect(page.getByTestId("superuser-user-overview-error")).toHaveCount(0); }); test("hides the group id field when the user has no positive group id", async ({ page }) => { const overviewApi = await setupOverviewApi(page); overviewApi.detailUser().group_id = null; await page.goto("/superuser/users/11", { waitUntil: "domcontentloaded" }); await expect(page.getByTestId("superuser-user-overview-group-id-label")).toHaveCount(0); overviewApi.detailUser().group_id = 0; await page.goto("/superuser/users/11", { waitUntil: "domcontentloaded" }); await expect(page.getByTestId("superuser-user-overview-group-id-label")).toHaveCount(0); }); test("uses localized customer label for current role when the user has no positive role id", async ({ page }) => { const overviewApi = await setupOverviewApi(page); overviewApi.detailUser().group_id = 0; await page.goto("/superuser/users/11/security", { waitUntil: "domcontentloaded" }); await expect(page.getByTestId("superuser-user-security-current-role")).toHaveText(/Customer|Kunde/); await expect(page.getByTestId("superuser-user-security-no-role")).toBeVisible(); await expect(page.getByTestId("superuser-user-security-page")).not.toContainText("#0"); }); test("manages customer rules on the security page when attributes are returned as plain strings", async ({ page, }) => { const overviewApi = await setupOverviewApi(page); overviewApi.detailUser().attributes = ["invoiceAllOrdersIndividually"]; await page.goto("/superuser/users/11/security", { waitUntil: "domcontentloaded" }); await expect(page.getByTestId("superuser-user-security-attribute-invoiceAllOrdersIndividually")).toContainText( /Invoice per order|Fakturer alle ordrer enkeltvis/ ); await expect(page.getByTestId("superuser-user-security-rule-toggle-invoiceAllOrdersIndividually")).toBeChecked(); await page.locator('label[for="superuser-user-security-rule-toggle-restrictSpotFree"]').click(); await expect .poll(() => overviewApi.customerAttributeCalls()) .toContainEqual({ action: "add", attribute: "restrictSpotFree", }); await expect(page.getByTestId("superuser-user-security-rule-toggle-restrictSpotFree")).toBeChecked(); await page.goto("/superuser/users/11/orders", { waitUntil: "domcontentloaded" }); await expect(page.getByTestId("superuser-user-orders-summary")).toContainText( /Invoice per order|Fakturer alle ordrer enkeltvis/ ); }); test("top security tiles expose hover actions and focus their sections", async ({ page }) => { await setupOverviewApi(page); await page.goto("/superuser/users/11/security", { waitUntil: "domcontentloaded" }); await page.getByTestId("superuser-user-security-metric-permissions").hover(); await expect(page.getByTestId("superuser-user-security-permissions-action")).toContainText( /Review permissions|Gennemgå tilladelser/ ); await page.getByTestId("superuser-user-security-metric-permissions").click(); await expect(page.getByTestId("superuser-user-security-access")).toBeInViewport(); await page.getByTestId("superuser-user-security-summary").scrollIntoViewIfNeeded(); await page.getByTestId("superuser-user-security-metric-rules").hover(); await expect(page.getByTestId("superuser-user-security-rules-action")).toContainText( /Manage rules|Administrer regler/ ); await page.getByTestId("superuser-user-security-metric-rules").click(); await expect(page.getByTestId("superuser-user-security-rules")).toBeInViewport(); await page.getByTestId("superuser-user-security-summary").scrollIntoViewIfNeeded(); await page.getByTestId("superuser-user-security-metric-managed").hover(); await expect(page.getByTestId("superuser-user-security-metric-managed")).toContainText( /Limited backoffice|Begrænset backoffice/ ); await expect(page.getByTestId("superuser-user-security-managed-action")).toContainText( /Migrate employee|Migrer medarbejder/ ); await page.getByTestId("superuser-user-security-metric-managed").click(); await expect(page.getByTestId("superuser-user-security-role-modal")).toBeVisible(); await expect(page.getByTestId("superuser-user-security-limited-panel")).toBeVisible(); await expect(page.getByTestId("superuser-user-security-limited-unavailable")).toBeVisible(); await page.getByTestId("superuser-user-security-role-cancel").click(); }); test("updates role assignment and assigned role permissions from the security page", async ({ page }) => { const overviewApi = await setupOverviewApi(page); await page.goto("/superuser/users/11/security", { waitUntil: "domcontentloaded" }); await expect(page.getByTestId("superuser-user-security-current-role")).toContainText("Administrators"); await expect(page.getByTestId("superuser-user-security-role-assignment")).toHaveCount(0); await page.getByTestId("superuser-user-security-metric-role").hover(); await expect(page.getByTestId("superuser-user-security-role-switch-action")).toContainText( /Switch role|Skift rolle/ ); await page.getByTestId("superuser-user-security-metric-role").click(); await expect(page.getByTestId("superuser-user-security-role-modal")).toBeVisible(); await expect(page.getByTestId("superuser-user-security-role-tab-direct")).toBeVisible(); await expect(page.getByTestId("superuser-user-security-role-tab-limited")).toBeVisible(); const roleModalBox = await page.getByTestId("superuser-user-security-role-modal").boundingBox(); const roleCloseBox = await page.getByTestId("superuser-user-security-role-modal-close").boundingBox(); const roleSaveBox = await page.getByTestId("superuser-user-security-role-save").boundingBox(); expect(roleModalBox).not.toBeNull(); expect(roleCloseBox).not.toBeNull(); expect(roleSaveBox).not.toBeNull(); expect(roleCloseBox?.y || 0).toBeGreaterThanOrEqual((roleModalBox?.y || 0) + 8); expect(roleCloseBox?.y || 0).toBeLessThan((roleModalBox?.y || 0) + 60); expect((roleCloseBox?.x || 0) + (roleCloseBox?.width || 0)).toBeLessThanOrEqual( (roleModalBox?.x || 0) + (roleModalBox?.width || 0) - 16 ); expect((roleSaveBox?.x || 0) + (roleSaveBox?.width || 0)).toBeLessThanOrEqual( (roleModalBox?.x || 0) + (roleModalBox?.width || 0) + 1 ); await page.getByTestId("superuser-user-security-role-tab-limited").click(); await expect(page.getByTestId("superuser-user-security-limited-unavailable")).toBeVisible(); await page.getByTestId("superuser-user-security-role-tab-direct").click(); await page.getByTestId("superuser-user-security-role-select").selectOption("2"); await page.getByTestId("superuser-user-security-role-save").click(); await expect .poll(() => overviewApi.lastUserUpdatePayload()) .toMatchObject({ id: 11, role: "2", }); await expect(page.getByTestId("superuser-user-security-current-role")).toContainText("Customer Support"); await expect(page.getByTestId("superuser-user-security-role-modal")).toHaveCount(0); await expect(page.getByTestId("superuser-user-security-role-save-success")).toBeVisible(); await page.getByRole("tab", { name: /Orders and POS|Ordrer og POS/ }).click(); await clickRolePermissionSwitch(page, "add_order"); await expect .poll(() => overviewApi.rolePermissionCalls()) .toContainEqual({ action: "add", groupId: 2, permission: "add_order", }); await expect(page.getByTestId("role-permission-toggle-add_order")).toBeChecked(); }); test("migrates an eligible user to limited backoffice from the role modal", async ({ page }) => { const overviewApi = await setupOverviewApi(page); overviewApi.detailUser().id = 13; overviewApi.detailUser().customer_number = 0; overviewApi.detailUser().display_name = "Legacy Clerk"; overviewApi.detailUser().limited_backoffice_managed = false; overviewApi.detailUser().economic_customer = { customerNumber: 0, name: "Legacy Clerk", }; let migrationPayload: unknown = null; await page.route(apiPathPattern("/limited-backoffice/departments"), async (route) => { await route.fulfill( json({ data: [ { id: 1, name: "Assigned Depot" }, { id: 2, name: "Remote Depot" }, ], }) ); }); await page.route(apiPathPattern("/limited-backoffice/roles"), async (route) => { await route.fulfill( json({ data: [ { key: "viewer", label: "Viewer" }, { key: "cashier", label: "Cashier" }, { key: "department_admin", label: "Department admin" }, ], }) ); }); await page.route(apiPathPattern("/limited-backoffice/employees/13/migrate"), async (route) => { migrationPayload = await route.request().postDataJSON(); overviewApi.detailUser().limited_backoffice_managed = true; await route.fulfill( json({ data: { id: 13, user_id: 13, customer_number: 0, display_name: "Legacy Clerk", role: { key: "cashier", label: "Cashier" }, departments: [{ id: 1, name: "Assigned Depot" }], active: true, }, }) ); }); await page.goto("/superuser/users/13/security", { waitUntil: "domcontentloaded" }); await expect(page.getByTestId("superuser-user-security-metric-managed")).toContainText( /Limited backoffice|Begrænset backoffice/ ); await page.getByTestId("superuser-user-security-metric-managed").hover(); await expect(page.getByTestId("superuser-user-security-managed-action")).toContainText( /Migrate employee|Migrer medarbejder/ ); await page.getByTestId("superuser-user-security-metric-managed").click(); await expect(page.getByTestId("superuser-user-security-limited-panel")).toBeVisible(); await expect(page.getByTestId("superuser-user-security-limited-unavailable")).toHaveCount(0); await expect(page.getByTestId("superuser-user-security-limited-role-select")).toHaveValue("cashier"); await page.locator('[data-testid="superuser-user-security-limited-department-1"] input').check(); await page.getByTestId("superuser-user-security-limited-migrate").click(); await expect.poll(() => migrationPayload).toEqual({ role_key: "cashier", department_ids: [1] }); await expect(page.getByTestId("superuser-user-security-role-modal")).toHaveCount(0); await expect(page.getByTestId("superuser-user-security-limited-success")).toBeVisible(); await expect(page.getByTestId("superuser-user-security-metric-managed")).toContainText(/Yes|Ja/); }); test("locks role assignment for limited backoffice managed users", async ({ page }) => { const overviewApi = await setupOverviewApi(page); overviewApi.detailUser().limited_backoffice_managed = true; await page.goto("/superuser/users/11/security", { waitUntil: "domcontentloaded" }); await expect(page.getByTestId("superuser-user-security-metric-managed")).toContainText(/Yes|Ja/); await expect(page.getByTestId("superuser-user-security-role-switch-action")).toHaveCount(0); await page.getByTestId("superuser-user-security-metric-role").click(); await expect(page.getByTestId("superuser-user-security-role-modal")).toHaveCount(0); expect(overviewApi.lastUserUpdatePayload()).toBeNull(); }); test("renders the user-specific subpages with named headers and contextual summaries", async ({ page }, testInfo) => { const overviewApi = await setupOverviewApi(page); const expectMultipleMetricColumns = !/mobile/i.test(testInfo.project.name); const expectMultiplePanelColumns = isDesktopProject(testInfo); await page.goto("/superuser/users/11/orders", { waitUntil: "domcontentloaded" }); await expectUserDetailShell(page, "orders"); await expect(page).toHaveTitle("Anna Transport | Truck Wash"); await expect( page.getByText( /Invoice handling, open drafts, and order history|Fakturahåndtering, åbne kladder og ordrehistorik/ ) ).toBeVisible(); await expect(page.getByTestId("superuser-user-orders-summary")).toContainText( /Invoice per order|Fakturer alle ordrer enkeltvis/ ); await expect(page.getByTestId("superuser-user-orders-summary")).toContainText("DRAFT-1"); await expectTileGridLayout(page, "superuser-user-orders-metrics", expectMultipleMetricColumns); await page.goto("/superuser/users/11/pricing", { waitUntil: "domcontentloaded" }); await expectUserDetailShell(page, "pricing"); await expect(page).toHaveTitle("Anna Transport | Truck Wash"); await expect( page.getByText( /Customer-specific discounts and fixed product prices|Kundespecifikke rabatter og faste produktpriser/ ) ).toBeVisible(); await expect(page.getByTestId("superuser-user-pricing-summary")).toContainText("10%"); await expectTileGridLayout(page, "superuser-user-pricing-metrics", expectMultipleMetricColumns); await expect(page.getByTestId("superuser-user-pricing-table")).toContainText("Foam Deluxe"); await page.goto("/superuser/users/11/other", { waitUntil: "domcontentloaded" }); await expectUserDetailShell(page, "other"); await expect(page).toHaveTitle("Anna Transport | Truck Wash"); await expect( page.getByText(/Special arrangements and internal customer notes|Særlige aftaler og interne kundenoter/) ).toBeVisible(); await expectTileGridLayout(page, "superuser-user-other-tiles", expectMultiplePanelColumns); await expect(page.getByTestId("superuser-user-other-default-department")).toBeVisible(); await expect(page.getByTestId("superuser-user-other-fixed-pricing")).toContainText(/1\.234|1,234/); await expect(page.getByTestId("superuser-user-special-arrangement-input")).toHaveValue("Night wash agreement"); await expect(page.getByTestId("superuser-user-wash-subscription-note-input")).toHaveValue( "Monthly wash subscription note" ); await expect(page.getByTestId("superuser-user-other-customer-flags")).toBeVisible(); await page.goto("/superuser/users/11/security", { waitUntil: "domcontentloaded" }); await expectUserDetailShell(page, "security", "superuser-user-security-page"); await expect(page).toHaveTitle("Anna Transport | Truck Wash"); await expect( page.getByText(/Permissions and customer rule access|Tilladelser og adgang til kunderegler/) ).toBeVisible(); await page.getByRole("tab", { name: /Products and pricing|Produkter og priser/ }).click(); await expect(page.getByTestId("role-permission-row-set_custom_price")).toContainText("set_custom_price"); await expect(page.getByTestId("superuser-user-security-rules")).toContainText( /Invoice per order|Fakturer alle ordrer enkeltvis/ ); await page.goto("/superuser/users/11/vehicles", { waitUntil: "domcontentloaded" }); await expectUserDetailShell(page, "vehicles", "superuser-user-vehicles-page"); await expect(page).toHaveTitle("Anna Transport | Truck Wash"); await expect( page.getByText( /Vehicles, subscriptions, and mass registration tools|Køretøjer, abonnementer og masseregistrering/ ) ).toBeVisible(); await expectTileGridLayout(page, "superuser-user-vehicles-tiles", expectMultiplePanelColumns); await expect .poll(() => overviewApi .vehicleRequests() .some( (request) => request.method === "GET" && request.pathname.endsWith("/superuser/users/11/vehicles/summary") ) ) .toBe(true); await expectTileGridLayout(page, "superuser-user-vehicles-metrics", expectMultipleMetricColumns); await expect(page.getByTestId("superuser-user-vehicles-summary")).toContainText("2"); await expect(page.getByTestId("superuser-user-vehicles-summary")).toContainText("1"); await expect(page.getByTestId("superuser-user-vehicles-subscription-invoicing")).toContainText("694"); await expect(page.getByTestId("superuser-user-vehicles-mass-insert")).toBeVisible(); const vehiclesPanel = page.getByTestId("superuser-user-vehicles-table-panel"); await expect(vehiclesPanel).toContainText("AA11223"); await expect(vehiclesPanel).toContainText("BB44556"); if (isDesktopProject(testInfo)) { await expect(vehiclesPanel).not.toContainText(/Anna Transport/); } await page.goto("/superuser/users/11/subusers", { waitUntil: "domcontentloaded" }); await expectUserDetailShell(page, "subusers", "superuser-user-subusers-page"); await expect(page).toHaveTitle("Anna Transport | Truck Wash"); await expect(page.getByText(/Drivers and customer access grants|Chauffører og kundeadgange/)).toBeVisible(); await expect .poll(() => overviewApi .subuserRequests() .some( (request) => request.method === "GET" && request.pathname.endsWith("/superuser/users/11/subusers/summary") ) ) .toBe(true); await expectTileGridLayout(page, "superuser-user-subusers-metrics", expectMultipleMetricColumns); await expect(page.getByTestId("superuser-user-subusers-summary")).toContainText("2"); const subusersPanel = page.getByTestId("superuser-user-subusers-table-panel"); await expect(subusersPanel).toContainText("Driver One"); if (!isDesktopProject(testInfo)) { await expect(subusersPanel).toContainText(/Anna Transport/); } await page.goto("/superuser/users/11/xlvask", { waitUntil: "domcontentloaded" }); await expectUserDetailShell(page, "xlvask"); await expect(page).toHaveTitle("Anna Transport | Truck Wash"); await expect( page.getByText( /XLVask account details and related imported vehicles|XLVask-kontooplysninger og importerede køretøjer/ ) ).toBeVisible(); await expectTileGridLayout(page, "superuser-user-xlvask-tiles", expectMultiplePanelColumns); await expect(page.getByTestId("superuser-user-xlvask-summary")).toContainText("Anna XLVask"); await expect(page.getByTestId("superuser-user-xlvask-vehicles")).toContainText("XL11223"); }); test("invites drivers from the user-scoped subusers page without sending a customer number", async ({ page }) => { await setupOverviewApi(page); await page.goto("/superuser/users/11/subusers", { waitUntil: "domcontentloaded" }); await expectUserDetailShell(page, "subusers", "superuser-user-subusers-page"); await page.getByTestId("superuser-user-subusers-invite").click(); await page.locator("#subuser-form-name").fill("Scoped Invite Driver"); await page.locator("#subuser-form-phone-country-code").fill("45"); await page.locator("#subuser-form-phone").fill("70300300"); const inviteRequestPromise = page.waitForRequest((request) => { const url = new URL(request.url()); return request.method() === "POST" && url.pathname.endsWith("/superuser/users/11/subusers/invite"); }); await page.getByRole("button", { name: /Send invitation|Send invitation|Send invitation/i }).click(); const inviteRequest = await inviteRequestPromise; const inviteDialog = page.locator(".swal2-popup").filter({ hasText: "Chauffør oprettet" }); await expect(inviteDialog.locator(".swal2-html-container")).toContainText(/Invitationen blev sendt på SMS/i); expect(inviteRequest.postDataJSON()).toMatchObject({ name: "Scoped Invite Driver", phone_country_code: 45, phone: 70300300, }); expect(inviteRequest.postDataJSON()).not.toHaveProperty("customer_number"); }); test("adds vehicles from the user-scoped vehicles page without changing customer scope", async ({ page }) => { const overviewApi = await setupOverviewApi(page); await page.goto("/superuser/users/11/vehicles", { waitUntil: "domcontentloaded" }); await expectUserDetailShell(page, "vehicles", "superuser-user-vehicles-page"); await page.getByTestId("superuser-user-vehicles-add").click(); await page.locator("#type").selectOption("101"); await page.locator("#reg").fill("CC77889"); await page.locator("#wash_subscription").selectOption("true"); const vehicleRequestPromise = page.waitForRequest((request) => { const url = new URL(request.url()); return request.method() === "POST" && url.pathname.endsWith("/superuser/users/11/vehicles"); }); await page.locator(".swal2-confirm").click(); const vehicleRequest = await vehicleRequestPromise; expect(vehicleRequest.postDataJSON()).toMatchObject({ customer_id: 12345, reg: "CC77889", type: 101, wash_subscription: true, }); expect(overviewApi.vehicleRequests()).toContainEqual( expect.objectContaining({ method: "POST", pathname: expect.stringMatching(/\/superuser\/users\/11\/vehicles$/), }) ); }); });