Files
pleno-vue/tests/e2e/subuser-management.spec.ts
T
Jeppe B eee9ba1c13 Fix localized forbidden assertions in full E2E (#243)
Use the stable localized forbidden-page contract in customer, subuser, and superuser denial tests while retaining the protected-action and zero-request assertions.
2026-08-02 12:24:36 +02:00

1932 lines
74 KiB
TypeScript

import { expect, test, type Locator, type Page, type Route } from "@playwright/test";
type MockSubuser = {
id: number;
customer_number?: number;
customer_name?: string;
username: string | null;
name: string | null;
email: string | null;
email_verified?: boolean;
email_verified_at?: string | null;
phone_country_code: number | null;
phone: number | null;
phone_verified?: boolean;
phone_verified_at?: string | null;
verification_state?: string;
created_at: string;
updated_at: string;
suspended_at: string | null;
two_factor_enabled: boolean;
setup_required: boolean;
invite_accepted: boolean;
can_resend_invite: boolean;
profile_editable_by_manager: boolean;
grant_id: number;
grant_enabled: boolean;
grant_note: string | null;
grant_permissions: string[];
permission_template_key?: string;
permission_groups?: Array<{ key: string; capabilities: string[] }>;
assigned_vehicle_id?: number | null;
assigned_vehicle_reg?: string | null;
assigned_vehicle?: { id: number; reg: string; reference?: string | null } | null;
dognvask_enabled?: boolean;
grants?: MockSubuserGrant[];
grant_count?: number;
customer_numbers?: number[];
access_state: "active" | "pending_setup" | "disabled" | "inactive";
};
type MockSubuserGrant = Pick<
MockSubuser,
| "customer_number"
| "customer_name"
| "grant_id"
| "grant_enabled"
| "grant_note"
| "grant_permissions"
| "permission_template_key"
| "permission_groups"
| "assigned_vehicle_id"
| "assigned_vehicle_reg"
| "assigned_vehicle"
| "dognvask_enabled"
| "access_state"
>;
const API_PATTERN = /https?:\/\/(?:api\.truckwash\.io(?::\d+)?|localhost(?::\d+)?\/api|127\.0\.0\.1(?::\d+)?\/api)\//;
const DOGNVASK_PERMISSION_KEYS = ["SELFSERVE_LIST", "SELFSERVE_ADD"];
const baseUserSession = {
id: 12,
customer_number: 12345678,
group_id: 3,
email: "demo@truckwash.test",
phone: {
number: "11111111",
country_code: "45",
},
notifications: {
wash_certificate_email: null,
email_notifications_enabled: true,
sms_notifications_enabled: false,
},
created_at: "2026-04-14 09:00:00",
updated_at: "2026-04-14 09:00:00",
display_name: "Demo Company",
economic_customer: [],
};
const baseSubuserSession = {
id: 91,
username: "manager",
name: "Subuser Manager",
email: "manager@example.com",
phone_country_code: 45,
phone: 12345678,
created_at: "2026-04-14 09:00:00",
updated_at: "2026-04-14 09:00:00",
suspended_at: null,
two_factor_enabled: false,
grants: [
{
name: "Demo Company",
billing_customer_number: 12345678,
permissions: ["SUBUSERS_LIST", "SUBUSERS_ADD", "SUBUSERS_EDIT", "SUBUSERS_DELETE"],
},
],
};
const initialSubusers = (): MockSubuser[] => [
{
id: 1,
username: null,
name: "Pending Driver",
email: null,
email_verified: false,
email_verified_at: null,
phone_country_code: 45,
phone: 11111111,
phone_verified: false,
phone_verified_at: null,
verification_state: "missing_email",
created_at: "2026-04-14 08:00:00",
updated_at: "2026-04-14 08:00:00",
suspended_at: null,
two_factor_enabled: false,
setup_required: true,
invite_accepted: false,
can_resend_invite: true,
profile_editable_by_manager: false,
grant_id: 11,
grant_enabled: true,
grant_note: null,
grant_permissions: ["BOOKINGS_LIST", "SELFSERVE_LIST", "SELFSERVE_ADD"],
assigned_vehicle_id: 101,
assigned_vehicle_reg: "ab12345",
assigned_vehicle: { id: 101, reg: "ab12345", reference: "Pending truck" },
dognvask_enabled: true,
access_state: "pending_setup",
},
{
id: 2,
username: "driver.active",
name: "Active Driver",
email: "active@example.com",
email_verified: true,
email_verified_at: "2026-04-14 07:35:00",
phone_country_code: 45,
phone: 22222222,
phone_verified: false,
phone_verified_at: null,
verification_state: "partial",
created_at: "2026-04-14 07:30:00",
updated_at: "2026-04-14 07:30:00",
suspended_at: null,
two_factor_enabled: false,
setup_required: false,
invite_accepted: true,
can_resend_invite: false,
profile_editable_by_manager: false,
grant_id: 12,
grant_enabled: true,
grant_note: "Morgenhold",
grant_permissions: ["BOOKINGS_LIST", "VEHICLES_LIST"],
assigned_vehicle_id: 102,
assigned_vehicle_reg: "cd67890",
assigned_vehicle: { id: 102, reg: "cd67890", reference: "Morning truck" },
dognvask_enabled: false,
access_state: "active",
},
{
id: 3,
username: "driver.disabled",
name: "Disabled Driver",
email: "disabled@example.com",
email_verified: false,
email_verified_at: null,
phone_country_code: 45,
phone: 33333333,
phone_verified: false,
phone_verified_at: null,
verification_state: "unverified",
created_at: "2026-04-14 07:00:00",
updated_at: "2026-04-14 07:00:00",
suspended_at: null,
two_factor_enabled: false,
setup_required: false,
invite_accepted: true,
can_resend_invite: false,
profile_editable_by_manager: false,
grant_id: 13,
grant_enabled: false,
grant_note: "Sat på pause",
grant_permissions: ["BOOKINGS_LIST", "SELFSERVE_LIST", "SELFSERVE_ADD"],
assigned_vehicle_id: 103,
assigned_vehicle_reg: "zz11111",
assigned_vehicle: { id: 103, reg: "zz11111", reference: "Paused truck" },
dognvask_enabled: false,
access_state: "disabled",
},
];
const permissionTemplatePayload = {
templates: [
{
key: "deactivated",
label: "Deactivated",
description: "Keeps the driver linked without active access.",
enabled: false,
permissions: [],
permission_groups: [],
},
{
key: "driver",
label: "Driver",
description: "Can use self-service, bookings, vehicles, and orders.",
enabled: true,
permissions: ["VEHICLES_LIST", "SELFSERVE_LIST", "SELFSERVE_ADD", "BOOKINGS_LIST", "BOOKINGS_ADD", "ORDERS_LIST"],
permission_groups: [
{ key: "vehicles", capabilities: ["view_vehicles"] },
{ key: "selfserve", capabilities: ["view_selfserve", "start_selfserve"] },
{ key: "bookings", capabilities: ["view_bookings", "add_bookings"] },
{ key: "orders", capabilities: ["view_orders"] },
],
},
{
key: "booking_coordinator",
label: "Booking coordinator",
description: "Can coordinate bookings.",
enabled: true,
permissions: ["VEHICLES_LIST", "BOOKINGS_LIST", "BOOKINGS_ADD", "BOOKINGS_EDIT", "ORDERS_LIST"],
permission_groups: [
{ key: "vehicles", capabilities: ["view_vehicles"] },
{ key: "bookings", capabilities: ["view_bookings", "add_bookings", "edit_bookings"] },
{ key: "orders", capabilities: ["view_orders"] },
],
},
{
key: "fleet_admin",
label: "Fleet admin",
description: "Can manage drivers and customer access.",
enabled: true,
permissions: [
"VEHICLES_LIST",
"BOOKINGS_LIST",
"SUBUSERS_LIST",
"SUBUSERS_EDIT",
"SUBUSERS_DELETE",
"SUBUSERS_ADD",
],
permission_groups: [
{ key: "vehicles", capabilities: ["view_vehicles"] },
{ key: "bookings", capabilities: ["view_bookings"] },
{
key: "driver_management",
capabilities: ["view_drivers", "edit_driver_access", "disable_driver_access", "invite_drivers"],
},
],
},
],
groups: [
{ key: "vehicles", capabilities: ["view_vehicles", "edit_vehicles", "delete_vehicles", "add_vehicles"] },
{ key: "selfserve", capabilities: ["view_selfserve", "edit_selfserve", "delete_selfserve", "start_selfserve"] },
{ key: "bookings", capabilities: ["view_bookings", "edit_bookings", "delete_bookings", "add_bookings"] },
{ key: "orders", capabilities: ["view_orders", "edit_orders"] },
{
key: "driver_management",
capabilities: ["view_drivers", "edit_driver_access", "disable_driver_access", "invite_drivers"],
},
],
};
const templateByKey = (key: string) => permissionTemplatePayload.templates.find((template) => template.key === key);
const applyTemplate = (subuser: MockSubuser, key: string) => {
const template = templateByKey(key);
if (!template) {
return;
}
subuser.permission_template_key = template.key;
subuser.grant_enabled = template.enabled;
subuser.grant_permissions = [...template.permissions];
subuser.permission_groups = [...template.permission_groups];
};
const initialSuperuserSubusers = (): MockSubuser[] => [
{
id: 51,
customer_number: 12345678,
customer_name: "Nordic Transport",
username: null,
name: "Pending Super Driver",
email: null,
email_verified: false,
email_verified_at: null,
phone_country_code: 45,
phone: 55555555,
phone_verified: false,
phone_verified_at: null,
verification_state: "missing_email",
created_at: "2026-04-14 08:00:00",
updated_at: "2026-04-14 08:00:00",
suspended_at: null,
two_factor_enabled: false,
setup_required: true,
invite_accepted: false,
can_resend_invite: true,
profile_editable_by_manager: false,
grant_id: 510,
grant_enabled: true,
grant_note: null,
grant_permissions: ["BOOKINGS_LIST"],
assigned_vehicle_id: 5101,
assigned_vehicle_reg: "nt12345",
assigned_vehicle: { id: 5101, reg: "nt12345", reference: "Nordic truck" },
dognvask_enabled: false,
access_state: "pending_setup",
},
{
id: 51,
customer_number: 87654321,
customer_name: "City Logistics",
username: null,
name: "Pending Super Driver",
email: null,
email_verified: false,
email_verified_at: null,
phone_country_code: 45,
phone: 55555555,
phone_verified: false,
phone_verified_at: null,
verification_state: "missing_email",
created_at: "2026-04-14 08:00:00",
updated_at: "2026-04-14 08:00:00",
suspended_at: null,
two_factor_enabled: false,
setup_required: true,
invite_accepted: false,
can_resend_invite: true,
profile_editable_by_manager: false,
grant_id: 511,
grant_enabled: true,
grant_note: "Ekstra kunde",
grant_permissions: ["ORDERS_LIST", "SELFSERVE_LIST", "SELFSERVE_ADD"],
assigned_vehicle_id: 5111,
assigned_vehicle_reg: "cl98765",
assigned_vehicle: { id: 5111, reg: "cl98765", reference: "City truck" },
dognvask_enabled: true,
access_state: "pending_setup",
},
{
id: 52,
customer_number: 87654321,
customer_name: "City Logistics",
username: "city.driver",
name: "City Driver",
email: "city.driver@example.com",
email_verified: true,
email_verified_at: "2026-04-14 07:35:00",
phone_country_code: 45,
phone: 66666666,
phone_verified: false,
phone_verified_at: null,
verification_state: "partial",
created_at: "2026-04-14 07:30:00",
updated_at: "2026-04-14 07:30:00",
suspended_at: null,
two_factor_enabled: false,
setup_required: false,
invite_accepted: true,
can_resend_invite: false,
profile_editable_by_manager: false,
grant_id: 520,
grant_enabled: true,
grant_note: "Aftenhold",
grant_permissions: ["BOOKINGS_LIST", "VEHICLES_LIST", "SELFSERVE_LIST", "SELFSERVE_ADD"],
assigned_vehicle_id: 5201,
assigned_vehicle_reg: "cd22222",
assigned_vehicle: { id: 5201, reg: "cd22222", reference: "City evening" },
dognvask_enabled: true,
access_state: "active",
},
];
const jsonResponse = (route: Route, data: unknown, meta: Record<string, unknown> = {}) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
success: true,
data,
meta,
includes: [],
}),
});
const recalculateAccessState = (subuser: MockSubuser) => {
subuser.can_resend_invite = subuser.setup_required;
subuser.invite_accepted = !subuser.setup_required;
subuser.dognvask_enabled =
Boolean(subuser.grant_enabled) &&
DOGNVASK_PERMISSION_KEYS.every((permission) => subuser.grant_permissions.includes(permission));
if (subuser.grant_enabled) {
subuser.access_state = subuser.setup_required ? "pending_setup" : "active";
return;
}
subuser.access_state = "disabled";
};
const recalculateVerificationState = (subuser: MockSubuser) => {
if (!subuser.email && !subuser.phone) {
subuser.verification_state = "missing_contacts";
return;
}
if (!subuser.email) {
subuser.verification_state = "missing_email";
return;
}
if (!subuser.phone) {
subuser.verification_state = "missing_phone";
return;
}
if (subuser.email_verified && subuser.phone_verified) {
subuser.verification_state = "verified";
return;
}
if (subuser.email_verified || subuser.phone_verified) {
subuser.verification_state = "partial";
return;
}
subuser.verification_state = "unverified";
};
const assignVehicle = (
subuser: MockSubuser,
vehicles: Array<{ id: number; reg: string; reference?: string }>,
vehicleId: number | null
) => {
const vehicle = vehicleId ? vehicles.find((item) => item.id === vehicleId) : null;
subuser.assigned_vehicle_id = vehicle?.id ?? null;
subuser.assigned_vehicle_reg = vehicle?.reg ?? null;
subuser.assigned_vehicle = vehicle ? { ...vehicle } : null;
};
async function primeSession(
page: Page,
options: { token: string; isSubuser?: boolean; selectedCustomerNumber?: number }
) {
await page.addInitScript((session) => {
window.localStorage.setItem("token", session.token);
if (session.isSubuser) {
window.localStorage.setItem("is_subuser", "true");
} else {
window.localStorage.removeItem("is_subuser");
}
if (session.selectedCustomerNumber) {
window.localStorage.setItem("selected_customer_number", String(session.selectedCustomerNumber));
} else {
window.localStorage.removeItem("selected_customer_number");
}
}, options);
}
async function mockManagementApi(
page: Page,
{
userPermissions = ["user", "list_own_subusers", "add_own_subusers", "edit_own_subusers", "delete_own_subusers"],
subuserPermissions = ["SUBUSERS_LIST", "SUBUSERS_ADD", "SUBUSERS_EDIT", "SUBUSERS_DELETE"],
_isSubuser = false,
}: {
userPermissions?: string[];
subuserPermissions?: string[];
isSubuser?: boolean;
}
) {
const subusers = initialSubusers();
const vehicles = [
{ id: 101, reg: "ab12345", reference: "Pending truck" },
{ id: 102, reg: "cd67890", reference: "Morning truck" },
{ id: 103, reg: "zz11111", reference: "Paused truck" },
{ id: 104, reg: "ny44444", reference: "New assignment" },
];
let nextId = 100;
let nextGrantId = 1000;
const grantPayloads: Array<Record<string, unknown>> = [];
await page.route(API_PATTERN, async (route) => {
const request = route.request();
const url = new URL(request.url());
const pathname = url.pathname.replace(/^\/api(?=\/)/, "");
if (pathname === "/auth/session" && request.method() === "GET") {
return jsonResponse(route, {
...baseUserSession,
permissions: userPermissions,
});
}
if (pathname === "/subusers/me" && request.method() === "GET") {
return jsonResponse(route, {
...baseSubuserSession,
grants: baseSubuserSession.grants.map((grant) => ({
...grant,
permissions: subuserPermissions,
})),
});
}
if (pathname === "/subusers" && request.method() === "GET") {
return jsonResponse(route, subusers, {
pagination: {
page: 1,
per_page: 100,
total: subusers.length,
},
});
}
if (pathname === "/vehicles" && request.method() === "GET") {
return jsonResponse(route, vehicles, {
pagination: {
page: 1,
per_page: 100,
total: vehicles.length,
},
});
}
if (pathname === "/customers" && request.method() === "GET") {
const search = String(url.searchParams.get("search") || "").toLowerCase();
const customers = [
{
customer_number: 22222222,
name: "Added Customer",
city: "Aarhus",
},
{
customer_number: 12345678,
name: "Nordic Transport",
city: "Aarhus",
},
{
customer_number: 87654321,
name: "City Logistics",
city: "Copenhagen",
},
].filter((customer) => {
if (!search) {
return true;
}
return (
String(customer.customer_number).includes(search) ||
customer.name.toLowerCase().includes(search) ||
customer.city.toLowerCase().includes(search)
);
});
return jsonResponse(route, customers);
}
if (pathname === "/subusers" && request.method() === "PUT") {
return route.fulfill({
status: 403,
contentType: "application/json",
body: JSON.stringify({
success: false,
data: {
message: "Customers can only manage subuser grants. Drivers own their account profile.",
},
meta: {},
includes: [],
}),
});
}
if (pathname === "/subusers/invite" && request.method() === "POST") {
const payload = request.postDataJSON() as Record<string, string | number | null>;
const created: MockSubuser = {
id: nextId++,
username: null,
name: payload.name as string,
email: null,
phone_country_code: Number(payload.phone_country_code),
phone: Number(payload.phone),
created_at: "2026-04-14 10:00:00",
updated_at: "2026-04-14 10:00:00",
suspended_at: null,
two_factor_enabled: false,
setup_required: true,
invite_accepted: false,
can_resend_invite: true,
profile_editable_by_manager: false,
grant_id: nextGrantId++,
grant_enabled: true,
grant_note: null,
grant_permissions: [],
assigned_vehicle_id: null,
assigned_vehicle_reg: null,
assigned_vehicle: null,
dognvask_enabled: false,
access_state: "pending_setup",
};
applyTemplate(created, String(payload.permission_template_key || "driver"));
recalculateAccessState(created);
subusers.unshift(created);
return jsonResponse(route, {
subuser: created,
grant: { id: created.grant_id },
invite: {
setup_link: `https://truckwash.io/complete-registration?token=subuser-${created.id}`,
delivery: {
status: "unavailable",
message: "SMS blev ikke sendt i testmiljøet.",
},
},
});
}
if (pathname === "/subusers/invite/resend" && request.method() === "POST") {
const payload = request.postDataJSON() as Record<string, number>;
const target = subusers.find((subuser) => subuser.id === Number(payload.id));
if (!target) {
return route.fulfill({ status: 404, body: JSON.stringify({ success: false }) });
}
if (!target.setup_required) {
return route.fulfill({
status: 409,
contentType: "application/json",
body: JSON.stringify({
success: false,
data: {
message: "Driver account already accepted the invitation.",
},
meta: {},
includes: [],
}),
});
}
return jsonResponse(route, {
subuser: target,
grant: { id: target.grant_id },
invite: {
setup_link: `https://truckwash.io/complete-registration?token=resend-${target.id}`,
delivery: {
status: "sent",
message: "Invitation sendt igen.",
},
},
});
}
if (pathname === "/subusers/grants" && request.method() === "PUT") {
const payload = request.postDataJSON() as Record<string, unknown>;
grantPayloads.push(payload);
const target = subusers.find((subuser) => subuser.grant_id === Number(payload.id));
if (!target) {
return route.fulfill({ status: 404, body: JSON.stringify({ success: false }) });
}
if (Array.isArray(payload.permissions)) {
target.grant_permissions = payload.permissions as string[];
target.permission_template_key = "custom";
}
if (typeof payload.permission_template_key === "string") {
applyTemplate(target, payload.permission_template_key);
}
if (Object.prototype.hasOwnProperty.call(payload, "note")) {
target.grant_note = (payload.note as string) || null;
}
if (Object.prototype.hasOwnProperty.call(payload, "enabled")) {
target.grant_enabled = Boolean(payload.enabled);
}
if (Object.prototype.hasOwnProperty.call(payload, "assigned_vehicle_id")) {
assignVehicle(target, vehicles, payload.assigned_vehicle_id ? Number(payload.assigned_vehicle_id) : null);
}
recalculateAccessState(target);
target.updated_at = "2026-04-14 10:05:00";
return jsonResponse(route, {
id: target.grant_id,
enabled: target.grant_enabled,
note: target.grant_note,
permissions: target.grant_permissions,
});
}
if (pathname === "/subusers/permission-templates" && request.method() === "GET") {
return jsonResponse(route, permissionTemplatePayload);
}
if (pathname === "/subusers/me" && request.method() === "PUT") {
const payload = request.postDataJSON() as Record<string, unknown>;
return jsonResponse(route, {
...baseSubuserSession,
name: (payload.name as string) ?? baseSubuserSession.name,
email: (payload.email as string) ?? baseSubuserSession.email,
});
}
if (pathname === "/ping" && request.method() === "GET") {
return jsonResponse(route, { ok: true });
}
return route.fulfill({
status: 404,
contentType: "application/json",
body: JSON.stringify({
success: false,
data: {
message: `Unhandled API route in test: ${request.method()} ${url.pathname}`,
},
meta: {},
includes: [],
}),
});
});
return {
grantPayloads,
};
}
async function mockSuperuserManagementApi(page: Page) {
const subusers = initialSuperuserSubusers();
const vehiclesByCustomer: Record<number, Array<{ id: number; reg: string; reference?: string }>> = {
12345678: [
{ id: 5101, reg: "nt12345", reference: "Nordic truck" },
{ id: 5102, reg: "nt24680", reference: "Nordic spare" },
],
87654321: [
{ id: 5111, reg: "cl98765", reference: "City truck" },
{ id: 5201, reg: "cd22222", reference: "City evening" },
],
};
let nextId = 600;
let nextGrantId = 6000;
const invitePayloads: Array<Record<string, unknown>> = [];
const resendPayloads: Array<Record<string, unknown>> = [];
const profilePayloads: Array<Record<string, unknown>> = [];
const passwordPayloads: Array<Record<string, unknown>> = [];
const addGrantPayloads: Array<Record<string, unknown>> = [];
const grantPayloads: Array<Record<string, unknown>> = [];
const verificationPayloads: Array<Record<string, unknown>> = [];
const verificationStatePayloads: Array<Record<string, unknown>> = [];
const passwordGuidePayloads: Array<Record<string, unknown>> = [];
const loginLinkPayloads: Array<Record<string, unknown>> = [];
await page.route(API_PATTERN, async (route) => {
const request = route.request();
const url = new URL(request.url());
const pathname = url.pathname.replace(/^\/api(?=\/)/, "");
if (pathname === "/auth/session" && request.method() === "GET") {
return jsonResponse(route, {
...baseUserSession,
permissions: ["superuser"],
});
}
if (pathname === "/superuser/subusers" && request.method() === "GET") {
return jsonResponse(route, subusers, {
pagination: {
page: 1,
per_page: 100,
total: subusers.length,
},
});
}
if (pathname === "/vehicles" && request.method() === "GET") {
const filters = url.searchParams.get("filters") || "";
const customerMatch = filters.match(/customer_id:(\d+)/);
const customerNumber = customerMatch ? Number(customerMatch[1]) : 12345678;
const vehicles = vehiclesByCustomer[customerNumber] || [];
return jsonResponse(route, vehicles, {
pagination: {
page: 1,
per_page: 100,
total: vehicles.length,
},
});
}
if (pathname === "/customers" && request.method() === "GET") {
const search = String(url.searchParams.get("search") || "").toLowerCase();
const customers = [
{
customer_number: 22222222,
name: "Added Customer",
city: "Aarhus",
},
{
customer_number: 12345678,
name: "Nordic Transport",
city: "Aarhus",
},
{
customer_number: 87654321,
name: "City Logistics",
city: "Copenhagen",
},
].filter((customer) => {
if (!search) {
return true;
}
return (
String(customer.customer_number).includes(search) ||
customer.name.toLowerCase().includes(search) ||
customer.city.toLowerCase().includes(search)
);
});
return jsonResponse(route, customers);
}
const profileMatch = pathname.match(/^\/superuser\/subusers\/(\d+)$/);
if (profileMatch && request.method() === "PATCH") {
const subuserId = Number(profileMatch[1]);
const payload = request.postDataJSON() as Record<string, unknown>;
profilePayloads.push({ id: subuserId, payload });
subusers
.filter((subuser) => subuser.id === subuserId)
.forEach((subuser) => {
if (typeof payload.name === "string") {
subuser.name = payload.name;
}
if (typeof payload.email === "string") {
if (payload.email !== subuser.email) {
subuser.email_verified = false;
subuser.email_verified_at = null;
}
subuser.email = payload.email;
}
if (Object.prototype.hasOwnProperty.call(payload, "phone_country_code")) {
if (Number(payload.phone_country_code) !== subuser.phone_country_code) {
subuser.phone_verified = false;
subuser.phone_verified_at = null;
}
subuser.phone_country_code = Number(payload.phone_country_code);
}
if (Object.prototype.hasOwnProperty.call(payload, "phone")) {
if (Number(payload.phone) !== subuser.phone) {
subuser.phone_verified = false;
subuser.phone_verified_at = null;
}
subuser.phone = Number(payload.phone);
}
subuser.verification_state =
subuser.email_verified && subuser.phone_verified
? "verified"
: subuser.email_verified || subuser.phone_verified
? "partial"
: subuser.email
? "unverified"
: "missing_email";
subuser.updated_at = "2026-04-14 10:10:00";
});
return jsonResponse(route, subusers.find((subuser) => subuser.id === subuserId) || null);
}
const verificationMatch = pathname.match(/^\/superuser\/subusers\/(\d+)\/verification\/(email|phone)\/send$/);
if (verificationMatch && request.method() === "POST") {
const subuserId = Number(verificationMatch[1]);
const channel = verificationMatch[2];
verificationPayloads.push({ id: subuserId, channel });
const target = subusers.find((subuser) => subuser.id === subuserId) || null;
return jsonResponse(route, {
delivery: {
channel,
status: "sent",
message: "Verifikationskode sendt.",
},
subuser: target,
});
}
const verificationStateMatch = pathname.match(/^\/superuser\/subusers\/(\d+)\/verification\/(email|phone)$/);
if (verificationStateMatch && request.method() === "PATCH") {
const subuserId = Number(verificationStateMatch[1]);
const channel = verificationStateMatch[2] as "email" | "phone";
const payload = request.postDataJSON() as { verified?: boolean };
verificationStatePayloads.push({ id: subuserId, channel, payload });
const verifiedAt = payload.verified ? "2026-04-14 10:16:00" : null;
subusers
.filter((subuser) => subuser.id === subuserId)
.forEach((subuser) => {
if (channel === "email") {
subuser.email_verified = Boolean(payload.verified);
subuser.email_verified_at = verifiedAt;
} else {
subuser.phone_verified = Boolean(payload.verified);
subuser.phone_verified_at = verifiedAt;
}
recalculateVerificationState(subuser);
subuser.updated_at = "2026-04-14 10:16:00";
});
const target = subusers.find((subuser) => subuser.id === subuserId) || null;
return jsonResponse(route, {
result: {
channel,
status: payload.verified ? "verified" : "unverified",
verified_at: verifiedAt,
},
verification: target?.verification_state,
subuser: target,
});
}
const passwordMatch = pathname.match(/^\/superuser\/subusers\/(\d+)\/password$/);
if (passwordMatch && request.method() === "POST") {
const subuserId = Number(passwordMatch[1]);
passwordPayloads.push({
id: subuserId,
payload: request.postDataJSON() as Record<string, unknown>,
});
subusers
.filter((subuser) => subuser.id === subuserId)
.forEach((subuser) => {
subuser.setup_required = false;
recalculateAccessState(subuser);
subuser.updated_at = "2026-04-14 10:17:00";
});
return jsonResponse(route, { subuser: subusers.find((subuser) => subuser.id === subuserId) || null });
}
const loginLinkMatch = pathname.match(/^\/superuser\/subusers\/(\d+)\/login-link$/);
if (loginLinkMatch && request.method() === "POST") {
return jsonResponse(route, {
login_path: `/qr-login/subusers/${loginLinkMatch[1]}?token=test-token`,
});
}
const passwordGuideLinkMatch = pathname.match(
/^\/superuser\/subusers\/(\d+)\/password-guide\/(email|phone)\/send$/
);
if (passwordGuideLinkMatch && request.method() === "POST") {
const payload = request.postDataJSON() as Record<string, unknown>;
passwordGuidePayloads.push({
id: Number(passwordGuideLinkMatch[1]),
channel: passwordGuideLinkMatch[2],
payload,
});
return jsonResponse(route, {
subuser_id: Number(passwordGuideLinkMatch[1]),
customer_number: payload.customer_number,
login_path: `/login/qr?token=password-guide-${passwordGuideLinkMatch[1]}&type=subuser`,
login_url: `https://truckwash.io/login/qr?token=password-guide-${passwordGuideLinkMatch[1]}&type=subuser`,
delivery: {
channel: passwordGuideLinkMatch[2],
status: "sent",
message: "Guide til ny adgangskode er sendt.",
},
subuser: subusers.find((subuser) => subuser.id === Number(passwordGuideLinkMatch[1])) || null,
});
}
const sentLoginLinkMatch = pathname.match(/^\/superuser\/subusers\/(\d+)\/login-link\/(email|phone)\/send$/);
if (sentLoginLinkMatch && request.method() === "POST") {
const payload = request.postDataJSON() as Record<string, unknown>;
loginLinkPayloads.push({
id: Number(sentLoginLinkMatch[1]),
channel: sentLoginLinkMatch[2],
payload,
});
return jsonResponse(route, {
subuser_id: Number(sentLoginLinkMatch[1]),
customer_number: payload.customer_number,
login_path: `/login/qr?token=login-link-${sentLoginLinkMatch[1]}&type=subuser`,
login_url: `https://truckwash.io/login/qr?token=login-link-${sentLoginLinkMatch[1]}&type=subuser`,
delivery: {
channel: sentLoginLinkMatch[2],
status: "sent",
message: "Loginlink er sendt.",
},
subuser: subusers.find((subuser) => subuser.id === Number(sentLoginLinkMatch[1])) || null,
});
}
if (pathname === "/superuser/subusers/invite" && request.method() === "POST") {
const payload = request.postDataJSON() as Record<string, string | number | null>;
invitePayloads.push(payload);
const created: MockSubuser = {
id: nextId++,
customer_number: Number(payload.customer_number),
customer_name: "Nordic Transport",
username: null,
name: payload.name as string,
email: null,
email_verified: false,
email_verified_at: null,
phone_country_code: Number(payload.phone_country_code),
phone: Number(payload.phone),
phone_verified: false,
phone_verified_at: null,
verification_state: "missing_email",
created_at: "2026-04-14 10:00:00",
updated_at: "2026-04-14 10:00:00",
suspended_at: null,
two_factor_enabled: false,
setup_required: true,
invite_accepted: false,
can_resend_invite: true,
profile_editable_by_manager: false,
grant_id: nextGrantId++,
grant_enabled: true,
grant_note: null,
grant_permissions: [],
assigned_vehicle_id: null,
assigned_vehicle_reg: null,
assigned_vehicle: null,
dognvask_enabled: false,
access_state: "pending_setup",
};
applyTemplate(created, String(payload.permission_template_key || "driver"));
recalculateAccessState(created);
subusers.unshift(created);
return jsonResponse(route, {
subuser: created,
grant: { id: created.grant_id },
invite: {
setup_link: `https://truckwash.io/complete-registration?token=superuser-${created.id}`,
delivery: {
status: "unavailable",
message: "SMS blev ikke sendt i testmiljøet.",
},
},
});
}
if (pathname === "/subusers/grants" && request.method() === "POST") {
const payload = request.postDataJSON() as Record<string, unknown>;
addGrantPayloads.push(payload);
const source = subusers.find((subuser) => subuser.id === Number(payload.subuser_id));
if (!source) {
return route.fulfill({ status: 404, body: JSON.stringify({ success: false }) });
}
const customerNumber = Number(payload.customer_number);
const created: MockSubuser = {
...source,
customer_number: customerNumber,
customer_name: customerNumber === 22222222 ? "Added Customer" : `Kunde ${customerNumber}`,
grant_id: nextGrantId++,
grant_enabled: payload.enabled !== false,
grant_note: typeof payload.note === "string" ? payload.note : null,
grant_permissions: [],
permission_template_key: undefined,
permission_groups: [],
assigned_vehicle_id: null,
assigned_vehicle_reg: null,
assigned_vehicle: null,
dognvask_enabled: false,
};
applyTemplate(created, String(payload.permission_template_key || "driver"));
if (Object.prototype.hasOwnProperty.call(payload, "assigned_vehicle_id")) {
const vehicles = vehiclesByCustomer[customerNumber] || [];
assignVehicle(created, vehicles, payload.assigned_vehicle_id ? Number(payload.assigned_vehicle_id) : null);
}
recalculateAccessState(created);
created.updated_at = "2026-04-14 10:14:00";
subusers.push(created);
return jsonResponse(route, {
subuser: created,
grant: { id: created.grant_id },
});
}
if (pathname === "/subusers/grants" && request.method() === "PUT") {
const payload = request.postDataJSON() as Record<string, unknown>;
grantPayloads.push(payload);
const target = subusers.find((subuser) => subuser.grant_id === Number(payload.id));
if (!target) {
return route.fulfill({ status: 404, body: JSON.stringify({ success: false }) });
}
if (Array.isArray(payload.permissions)) {
target.grant_permissions = payload.permissions as string[];
target.permission_template_key = "custom";
}
if (typeof payload.permission_template_key === "string" && payload.permission_template_key !== "custom") {
applyTemplate(target, payload.permission_template_key);
}
if (Object.prototype.hasOwnProperty.call(payload, "note")) {
target.grant_note = (payload.note as string) || null;
}
if (Object.prototype.hasOwnProperty.call(payload, "enabled")) {
target.grant_enabled = Boolean(payload.enabled);
}
if (Object.prototype.hasOwnProperty.call(payload, "assigned_vehicle_id")) {
const vehicles = vehiclesByCustomer[Number(target.customer_number)] || [];
assignVehicle(target, vehicles, payload.assigned_vehicle_id ? Number(payload.assigned_vehicle_id) : null);
}
recalculateAccessState(target);
target.updated_at = "2026-04-14 10:15:00";
return jsonResponse(route, {
id: target.grant_id,
enabled: target.grant_enabled,
note: target.grant_note,
permissions: target.grant_permissions,
assigned_vehicle_id: target.assigned_vehicle_id,
});
}
if (pathname === "/superuser/subusers/invite/resend" && request.method() === "POST") {
const payload = request.postDataJSON() as Record<string, number>;
resendPayloads.push(payload);
const target = subusers.find((subuser) => subuser.id === Number(payload.id));
if (!target) {
return route.fulfill({ status: 404, body: JSON.stringify({ success: false }) });
}
return jsonResponse(route, {
subuser: target,
grant: { id: target.grant_id },
invite: {
setup_link: `https://truckwash.io/complete-registration?token=resend-${target.id}`,
delivery: {
status: "sent",
message: "Invitation sendt igen.",
},
},
});
}
if (pathname === "/ping" && request.method() === "GET") {
return jsonResponse(route, { ok: true });
}
if (pathname === "/subusers/permission-templates" && request.method() === "GET") {
return jsonResponse(route, permissionTemplatePayload);
}
return route.fulfill({
status: 404,
contentType: "application/json",
body: JSON.stringify({
success: false,
data: {
message: `Unhandled API route in test: ${request.method()} ${url.pathname}`,
},
meta: {},
includes: [],
}),
});
});
return {
invitePayloads,
resendPayloads,
profilePayloads,
passwordPayloads,
addGrantPayloads,
grantPayloads,
verificationPayloads,
verificationStatePayloads,
passwordGuidePayloads,
loginLinkPayloads,
};
}
async function openSubuserActions(page: Page, rowKey: number | string) {
const actions = page.getByTestId(`subuser-actions-${rowKey}`);
const dropdown = actions.locator(".dropdown-content");
const trigger = actions.locator(".action-settings-wheel-trigger");
await expect(trigger).toBeVisible();
if (!(await dropdown.isVisible())) {
await page.keyboard.press("Escape").catch(() => {});
await page.mouse.move(0, 0).catch(() => {});
await page.waitForTimeout(150);
await trigger.scrollIntoViewIfNeeded();
for (let attempt = 0; attempt < 3; attempt += 1) {
await trigger.click({ force: attempt > 0, timeout: 2_000 }).catch(async () => {
await trigger.focus();
await page.keyboard.press("Enter").catch(() => {});
await trigger.evaluate((element) => {
if (element instanceof HTMLElement) {
element.click();
}
});
});
if (await dropdown.isVisible({ timeout: 1_000 }).catch(() => false)) {
break;
}
}
}
await expect(dropdown).toBeVisible();
return actions;
}
async function activateSubuserActionSection(actions: Locator, sectionKey: string) {
const section = actions.getByTestId(`action-settings-wheel-section-${sectionKey}`);
await expect(section).toBeVisible();
if (
await actions
.getByTestId("action-settings-wheel-flyout")
.isVisible()
.catch(() => false)
) {
await section.hover();
await section.click();
}
}
async function activateSubuserNestedSection(actions: Locator, testId: string) {
const subsection = actions.getByTestId(testId);
await expect(subsection).toBeVisible();
if (
await actions
.getByTestId("action-settings-wheel-flyout")
.isVisible()
.catch(() => false)
) {
await subsection.hover();
await subsection.click();
const panel = actions.getByTestId(`${testId}-panel`);
await expect(panel).toBeVisible();
return panel;
}
return subsection;
}
async function activateSubuserContactSection(actions: Locator, rowKey: number | string, channel: "email" | "phone") {
const contactSection = actions.getByTestId(`subuser-contact-${channel}-section-${rowKey}`);
await expect(contactSection).toBeVisible();
if (
await actions
.getByTestId("action-settings-wheel-flyout")
.isVisible()
.catch(() => false)
) {
await contactSection.hover();
await contactSection.click();
const contactPanel = actions.getByTestId(`subuser-contact-${channel}-section-${rowKey}-panel`);
await expect(contactPanel).toBeVisible();
return contactPanel;
}
return contactSection;
}
async function activateSubuserCustomerAccess(actions: Locator, rowKey: number | string) {
const customerSection = actions.getByTestId(`subuser-access-section-${rowKey}`);
await expect(customerSection).toBeVisible();
if (
await actions
.getByTestId("action-settings-wheel-flyout")
.isVisible()
.catch(() => false)
) {
await customerSection.hover();
await customerSection.click();
const customerPanel = actions.getByTestId(`subuser-access-section-${rowKey}-panel`);
await expect(customerPanel).toBeVisible();
const [customerPanelBox, accessSubmenuBox] = await Promise.all([
customerPanel.boundingBox(),
actions.getByTestId("action-settings-wheel-submenu-subuser-accesses").boundingBox(),
]);
if (!customerPanelBox || !accessSubmenuBox) {
throw new Error("Expected visible customer access panel and Adgange submenu boxes");
}
expect(customerPanelBox.height).toBeGreaterThanOrEqual(accessSubmenuBox.height - 1);
}
return customerSection;
}
async function expectLocatorBelow(lower: Locator, upper: Locator) {
const [lowerBox, upperBox] = await Promise.all([lower.boundingBox(), upper.boundingBox()]);
if (!lowerBox || !upperBox) {
throw new Error("Expected visible locators before comparing vertical order");
}
expect(lowerBox.y).toBeGreaterThanOrEqual(upperBox.y + upperBox.height - 1);
}
async function expectLocatorInside(container: Locator, child: Locator) {
const [containerBox, childBox] = await Promise.all([container.boundingBox(), child.boundingBox()]);
if (!containerBox || !childBox) {
throw new Error("Expected visible locators before comparing containment");
}
expect(childBox.x).toBeGreaterThanOrEqual(containerBox.x - 1);
expect(childBox.y).toBeGreaterThanOrEqual(containerBox.y - 1);
expect(childBox.x + childBox.width).toBeLessThanOrEqual(containerBox.x + containerBox.width + 1);
expect(childBox.y + childBox.height).toBeLessThanOrEqual(containerBox.y + containerBox.height + 1);
}
async function visibleWheelItem(actions: Locator, container: Locator, testId: string) {
const scoped = container.getByTestId(testId);
if (await scoped.isVisible().catch(() => false)) {
return scoped;
}
return actions.getByTestId(testId);
}
async function openSubuserAccessPanel(page: Page, rowKey: number | string) {
const trigger = page.getByTestId(`subuser-access-trigger-${rowKey}`);
await expect(trigger).toBeVisible();
await trigger.click();
const panel = page.getByTestId(`subuser-access-panel-${rowKey}`);
await expect(panel).toBeVisible();
return panel;
}
async function expectAccessPanelDoesNotOpenFromEmptyCellSpace(page: Page, rowKey: number | string) {
await page.keyboard.press("Escape");
const stack = page.getByTestId(`subuser-access-stack-${rowKey}`);
const trigger = page.getByTestId(`subuser-access-trigger-${rowKey}`);
const cell = stack.locator("xpath=ancestor::td[1]");
const [cellBox, triggerBox] = await Promise.all([cell.boundingBox(), trigger.boundingBox()]);
if (!cellBox || !triggerBox) {
throw new Error("Expected access cell and trigger boxes before empty-space hover check");
}
const emptyX = cellBox.x + cellBox.width - 4;
expect(emptyX).toBeGreaterThan(triggerBox.x + triggerBox.width + 2);
await page.mouse.move(emptyX, cellBox.y + cellBox.height / 2);
await expect(page.getByTestId(`subuser-access-panel-${rowKey}`)).toHaveCount(0);
}
async function expectAccessPanelOverlaysWithoutTableShift(page: Page, rowKey: number | string) {
await page.keyboard.press("Escape");
const trigger = page.getByTestId(`subuser-access-trigger-${rowKey}`);
const row = page.getByTestId(`subuser-access-stack-${rowKey}`).locator("xpath=ancestor::tr[1]");
const table = page.locator("table.subusers-table");
const [rowBefore, tableBefore] = await Promise.all([row.boundingBox(), table.boundingBox()]);
if (!rowBefore || !tableBefore) {
throw new Error("Expected row and table boxes before access panel hover");
}
await trigger.hover();
const panel = page.getByTestId(`subuser-access-panel-${rowKey}`);
await expect(panel).toBeVisible();
const [rowAfter, tableAfter] = await Promise.all([row.boundingBox(), table.boundingBox()]);
if (!rowAfter || !tableAfter) {
throw new Error("Expected row and table boxes after access panel hover");
}
expect(rowAfter.height).toBeLessThanOrEqual(rowBefore.height + 1);
expect(tableAfter.height).toBeLessThanOrEqual(tableBefore.height + 1);
return panel;
}
async function expectStructuredWheelRow(row: Locator, label: string, value: string) {
const labelLocator = row
.locator(".dropdown-item-action__label, .dropdown-item-label__text, .action-settings-wheel-select-item__label")
.first();
const detailLocator = row.locator(".dropdown-item-action__detail, .dropdown-item-label__detail").first();
const selectLocator = row.locator("select").first();
await expect(labelLocator).toHaveText(label);
if ((await selectLocator.count()) > 0) {
await expect(selectLocator.locator("option:checked")).toHaveText(value);
} else {
await expect(detailLocator).toHaveText(value);
}
const valueBoxLocator = (await selectLocator.count()) > 0 ? selectLocator : detailLocator;
const [labelBox, valueBox] = await Promise.all([labelLocator.boundingBox(), valueBoxLocator.boundingBox()]);
if (!labelBox || !valueBox) {
throw new Error("Expected visible label and detail boxes before comparing horizontal order");
}
expect(valueBox.x).toBeGreaterThan(labelBox.x);
}
async function expectVehicleAssignmentModalWithoutBodyScroll(page: Page) {
const popup = page.locator(".swal2-popup.subuser-vehicle-assignment-modal");
const htmlContainer = page.locator(".swal2-html-container.subuser-vehicle-assignment-html");
const select = page.locator("#subuser-assigned-vehicle");
await expect(popup).toBeVisible();
await expect(htmlContainer).toBeVisible();
await expect(select).toBeVisible();
const metrics = await page.evaluate(() => {
const findElement = (selector: string) => {
const element = document.querySelector(selector);
if (!(element instanceof HTMLElement)) {
throw new Error(`Expected ${selector} to be an HTMLElement`);
}
return element;
};
const measure = (element: HTMLElement) => {
const rect = element.getBoundingClientRect();
const styles = window.getComputedStyle(element);
return {
clientHeight: element.clientHeight,
clientWidth: element.clientWidth,
overflowX: styles.overflowX,
overflowY: styles.overflowY,
rectBottom: rect.bottom,
rectLeft: rect.left,
rectRight: rect.right,
rectTop: rect.top,
scrollHeight: element.scrollHeight,
scrollWidth: element.scrollWidth,
};
};
return {
assignment: measure(findElement(".subuser-vehicle-assignment")),
htmlContainer: measure(findElement(".swal2-html-container.subuser-vehicle-assignment-html")),
popup: measure(findElement(".swal2-popup.subuser-vehicle-assignment-modal")),
select: measure(findElement("#subuser-assigned-vehicle")),
};
});
expect(metrics.popup.scrollWidth).toBeLessThanOrEqual(metrics.popup.clientWidth + 1);
expect(metrics.htmlContainer.scrollWidth).toBeLessThanOrEqual(metrics.htmlContainer.clientWidth + 1);
expect(metrics.htmlContainer.scrollHeight).toBeLessThanOrEqual(metrics.htmlContainer.clientHeight + 1);
expect(metrics.htmlContainer.overflowX).not.toMatch(/auto|scroll/);
expect(metrics.htmlContainer.overflowY).not.toMatch(/auto|scroll/);
expect(metrics.select.rectLeft).toBeGreaterThanOrEqual(metrics.assignment.rectLeft - 1);
expect(metrics.select.rectRight).toBeLessThanOrEqual(metrics.assignment.rectRight + 1);
}
test("customer user with own-subuser permissions can invite and manage grant access from /user/subusers without editing the driver account", async ({
page,
}) => {
await primeSession(page, { token: "user-token" });
const { grantPayloads } = await mockManagementApi(page, {
userPermissions: ["user", "list_own_subusers", "add_own_subusers", "edit_own_subusers", "delete_own_subusers"],
});
await page.goto("/user/subusers");
await expect(page.getByRole("heading", { name: /underbrugere|chauffører/i }).first()).toBeVisible();
await expect(page.getByRole("button", { name: /Invit.*chauff/i })).toBeVisible();
await expect(page.getByText("Pending Driver")).toBeVisible();
await expect(page.getByText("Disabled Driver")).toBeVisible();
await expect(page.getByRole("columnheader", { name: "Chauffør" })).toBeVisible();
await expect(page.getByRole("columnheader", { name: "Tlf." })).toBeVisible();
await expect(page.getByRole("columnheader", { name: "Nummerplade" })).toBeVisible();
await expect(page.getByRole("columnheader", { name: "Start selvbetjening" })).toBeVisible();
await expect(page.getByTestId("subuser-phone-1")).toContainText("+45 11111111");
await expect(page.getByTestId("subuser-plate-1")).toHaveText("AB12345");
await expect(page.getByTestId("subuser-plate-2")).toHaveText("CD67890");
await expect(page.getByTestId("subuser-name-edit-1")).toHaveCount(0);
await expect(page.getByTestId("subuser-phone-edit-1")).toHaveCount(0);
await expect(page.getByTestId("subuser-resend-2")).toHaveCount(0);
await page.getByTestId("subuser-dognvask-tooltip-3").hover();
await expect(page.locator(".tooltip-content:visible")).toContainText("Kundeadgangen er deaktiveret");
await page.mouse.move(0, 0);
await page.locator(".b-tooltip", { has: page.getByTestId("subuser-remove-2") }).hover();
await expect(
page.locator(".tooltip-content:visible", { hasText: "Deaktiverer chaufførens kundeadgang" }).first()
).toContainText("Deaktiverer chaufførens kundeadgang");
const managerActions = await openSubuserActions(page, 2);
await activateSubuserActionSection(managerActions, "subuser-contact");
const managerEmailContact = await activateSubuserContactSection(managerActions, 2, "email");
await expect(managerEmailContact.getByTestId("subuser-action-email-verification-2").locator("select")).toHaveCount(0);
await expect(managerEmailContact.getByTestId("subuser-send-email-password-guide-2")).toHaveCount(0);
await expect(managerEmailContact.getByTestId("subuser-send-email-login-link-2")).toHaveCount(0);
await page.keyboard.press("Escape");
await page.getByRole("button", { name: /Invit.*chauff/i }).click();
await page.fill("#subuser-form-name", "Invited Driver");
await page.fill("#subuser-form-phone-country-code", "45");
await page.fill("#subuser-form-phone", "44444444");
await page.getByRole("button", { name: "Send invitation" }).click();
await expect(page.getByRole("heading", { name: "Chauffør oprettet" })).toBeVisible();
await page.getByRole("button", { name: "Luk" }).click();
await expect(page.getByText("Invited Driver")).toBeVisible();
await expect(page.getByTestId("subuser-name-edit-100")).toHaveCount(0);
await expect(page.getByTestId("subuser-phone-100")).toContainText("+45 44444444");
await expect(page.getByTestId("subuser-plate-100")).toHaveText("-");
let actions = await openSubuserActions(page, 100);
await expect(actions.getByTestId("action-settings-wheel-section-subuser-account")).toHaveCount(0);
await expect(actions.getByTestId("action-settings-wheel-section-subuser-contact")).toBeVisible();
await activateSubuserActionSection(actions, "subuser-accesses");
await expect(actions.getByTestId("action-settings-wheel-section-subuser-accesses")).toBeVisible();
await expect(actions.getByTestId("action-settings-wheel-section-subuser-details")).toBeVisible();
await expect(actions.getByTestId("subuser-access-section-100-1000")).toBeVisible();
await expect(actions).toContainText("Kontakt");
await expect(actions).toContainText("Adgange");
await expect(actions).toContainText("Detaljer");
await expect(actions.getByTestId("subuser-edit-name-100")).toHaveCount(0);
await expect(actions.getByTestId("subuser-add-access-100")).toHaveCount(0);
await activateSubuserCustomerAccess(actions, "100-1000");
await expectStructuredWheelRow(actions.getByTestId("subuser-assign-vehicle-100-1000"), "Nummerplade", "Ingen");
await expectStructuredWheelRow(actions.getByTestId("subuser-permissions-100-1000"), "Tilladelser", "Chauffør");
await expectStructuredWheelRow(actions.getByTestId("subuser-note-100-1000"), "Note", "Ingen");
await actions.getByTestId("subuser-assign-vehicle-100-1000").click();
await expectVehicleAssignmentModalWithoutBodyScroll(page);
await page.selectOption("#subuser-assigned-vehicle", "104");
await page.getByRole("button", { name: "Gem" }).click();
await expect(page.getByTestId("subuser-plate-100")).toHaveText("NY44444");
actions = await openSubuserActions(page, 100);
await activateSubuserActionSection(actions, "subuser-accesses");
await activateSubuserCustomerAccess(actions, "100-1000");
await actions.getByTestId("subuser-permissions-100-1000").click();
await expect(page.getByText("Adgangsprofil for Invited Driver")).toBeVisible();
await expect(page.getByTestId("permission-template-driver")).toBeVisible();
await expect(page.getByTestId("permission-template-custom")).toHaveCount(0);
await page.getByTestId("permission-template-booking_coordinator").click();
await page.getByRole("button", { name: "Gem" }).click();
actions = await openSubuserActions(page, 100);
await activateSubuserActionSection(actions, "subuser-accesses");
await activateSubuserCustomerAccess(actions, "100-1000");
await expect(actions).toContainText("Bookingkoordinator");
await expect(page.getByText(/SUBUSERS_LIST/)).toHaveCount(0);
await actions.getByTestId("subuser-toggle-100-1000").click();
await page.getByRole("button", { name: "Deaktivér" }).click();
await expect(page.getByTestId("subuser-dognvask-tooltip-100")).toBeVisible();
actions = await openSubuserActions(page, 100);
await activateSubuserActionSection(actions, "subuser-accesses");
await activateSubuserCustomerAccess(actions, "100-1000");
await actions.getByTestId("subuser-toggle-100-1000").click();
await page.getByRole("button", { name: "Genaktivér" }).click();
await expect(page.getByTestId("subuser-dognvask-100")).toBeVisible();
const dognvaskPayloadCount = grantPayloads.length;
await page.getByTestId("subuser-dognvask-100").click();
await expect.poll(() => grantPayloads.length).toBe(dognvaskPayloadCount + 1);
const dognvaskPayload = grantPayloads.at(-1) || {};
expect(dognvaskPayload).toEqual(
expect.objectContaining({
id: 1000,
permissions: expect.arrayContaining(["SELFSERVE_LIST", "SELFSERVE_ADD"]),
})
);
expect(dognvaskPayload).not.toHaveProperty("permission_template_key");
actions = await openSubuserActions(page, 100);
await activateSubuserActionSection(actions, "subuser-accesses");
await activateSubuserCustomerAccess(actions, "100-1000");
await actions.getByTestId("subuser-resend-100-1000").click();
await expect(page.getByRole("heading", { name: "Chauffør oprettet" })).toBeVisible();
await page.getByRole("button", { name: "Luk" }).click();
});
test("customer user without own-subuser permissions cannot access the chauffør page", async ({ page }) => {
await primeSession(page, { token: "user-token-no-subusers" });
await mockManagementApi(page, { userPermissions: ["user"] });
await page.goto("/user/subusers");
const forbidden = page.getByTestId("restricted-forbidden");
await expect(forbidden).toBeVisible();
await expect(forbidden.getByRole("heading", { name: "403" })).toBeVisible();
await expect(page.getByRole("button", { name: /Invit.*chauff/i })).toHaveCount(0);
});
test("authorized subuser managers can access the chauffør page and the legacy grants route redirects", async ({
page,
}) => {
await primeSession(page, {
token: "subuser-manager-token",
isSubuser: true,
selectedCustomerNumber: 12345678,
});
await mockManagementApi(page, {
isSubuser: true,
subuserPermissions: ["SUBUSERS_LIST", "SUBUSERS_ADD", "SUBUSERS_EDIT", "SUBUSERS_DELETE"],
});
await page.goto("/user/subusers/grants");
await expect(page).toHaveURL("/user/subusers");
await expect(page.locator("label.label", { hasText: "Vælg kunde" })).toBeVisible();
await expect(page.getByRole("button", { name: /Invit.*chauff/i })).toBeVisible();
});
test("superusers can list and invite chauffeurs across customers", async ({ page }, testInfo) => {
const isDesktopProject = testInfo.project.name.includes("desktop");
if (isDesktopProject) {
await page.setViewportSize({ width: 1600, height: 900 });
}
await primeSession(page, { token: "superuser-token" });
const {
invitePayloads,
resendPayloads,
profilePayloads,
passwordPayloads,
addGrantPayloads,
grantPayloads,
verificationPayloads,
verificationStatePayloads,
passwordGuidePayloads,
loginLinkPayloads,
} = await mockSuperuserManagementApi(page);
await page.goto("/superuser/subusers");
await expect(page.getByRole("heading", { name: "Chauffører" }).first()).toBeVisible({ timeout: 30_000 });
await expect(page.getByText("Pending Super Driver")).toHaveCount(1);
await expect(page.getByTestId("subuser-phone-51")).toContainText("+45 55555555");
await expect(page.getByTestId("subuser-plate-51")).toHaveText("NT12345");
const accessStack = page.getByTestId("subuser-access-stack-51");
await expect(accessStack).toBeVisible();
const nordicChip = page.getByTestId("subuser-access-chip-51-510");
const cityChip = page.getByTestId("subuser-access-chip-51-511");
await expect(nordicChip).toContainText("Nordic Transport");
await expect(cityChip).toContainText("City Logistics");
await expect(nordicChip).not.toHaveText("NT");
await expect(cityChip).not.toHaveText("CL");
await expect(page.getByTestId("subuser-access-stack-52")).toHaveCount(0);
await expect(page.getByTestId("subuser-dognvask-52")).toBeVisible();
let accessPanel: Locator;
if (isDesktopProject) {
await expectAccessPanelDoesNotOpenFromEmptyCellSpace(page, 51);
accessPanel = await expectAccessPanelOverlaysWithoutTableShift(page, 51);
} else {
accessPanel = await openSubuserAccessPanel(page, 51);
}
await expect(accessPanel.getByTestId("subuser-access-row-51-510")).toContainText("Nordic Transport");
await expect(accessPanel.getByTestId("subuser-access-row-51-511")).toContainText("City Logistics");
await expect(accessPanel.getByTestId("subuser-access-enabled-51-510")).toBeVisible();
await expect(accessPanel.getByTestId("subuser-dognvask-51-511")).toBeVisible();
await expectLocatorInside(accessPanel, accessPanel.getByTestId("subuser-permissions-51-510"));
await expectLocatorInside(accessPanel, accessPanel.getByTestId("subuser-note-51-510"));
await expectLocatorInside(accessPanel, accessPanel.getByTestId("subuser-resend-51-510"));
await page.keyboard.press("Escape");
let actions = await openSubuserActions(page, 51);
await expect(actions.getByTestId("action-settings-wheel-section-subuser-account")).toBeVisible();
await expect(actions.getByTestId("action-settings-wheel-section-subuser-contact")).toBeVisible();
await expect(actions.getByTestId("action-settings-wheel-section-subuser-accesses")).toBeVisible();
await expect(actions.getByTestId("action-settings-wheel-section-subuser-details")).toBeVisible();
await expect(actions.getByTestId("action-settings-wheel-section-subuser-shortcuts")).toHaveCount(0);
await expect(actions).toContainText("Konto");
await expect(actions).toContainText("Kontakt");
await expect(actions).toContainText("Adgange");
await expect(actions).toContainText("Detaljer");
await activateSubuserActionSection(actions, "subuser-accesses");
const nordicAccess = await activateSubuserCustomerAccess(actions, "51-510");
await expect(nordicAccess).toContainText("Nordic Transport");
await expect(nordicAccess).toContainText("12345678");
await expect(nordicAccess).toContainText("Inaktiv");
await expectStructuredWheelRow(actions.getByTestId("subuser-assign-vehicle-51-510"), "Nummerplade", "NT12345");
await expect(actions.getByTestId("subuser-resend-51-510")).toBeVisible();
await actions.getByTestId("subuser-resend-51-510").click();
await expect(page.getByRole("heading", { name: "Chauffør oprettet" })).toBeVisible();
await page.getByRole("button", { name: "Luk" }).click();
expect(resendPayloads).toContainEqual({ id: 51, grant_id: 510, customer_number: 12345678 });
actions = await openSubuserActions(page, 51);
await activateSubuserActionSection(actions, "subuser-accesses");
const cityAccess = await activateSubuserCustomerAccess(actions, "51-511");
await expect(cityAccess).toContainText("City Logistics");
await expect(cityAccess).toContainText("87654321");
await expect(cityAccess).toContainText("Inaktiv");
await expectStructuredWheelRow(actions.getByTestId("subuser-note-51-511"), "Note", "Ekstra kunde");
await expectLocatorBelow(actions.getByTestId("subuser-toggle-51-511"), actions.getByTestId("subuser-note-51-511"));
const addAccessAction = actions.getByTestId("subuser-add-access-51");
await expect(addAccessAction).toBeVisible();
await expectLocatorBelow(addAccessAction, cityAccess);
await expect(page.getByTestId("subuser-name-edit-51")).toHaveCount(0);
await expect(page.getByTestId("subuser-phone-edit-51")).toHaveCount(0);
await expect(page.getByTestId("subuser-plate-edit-51")).toHaveCount(0);
actions = await openSubuserActions(page, 51);
await activateSubuserActionSection(actions, "subuser-details");
const identityDetails = await activateSubuserNestedSection(actions, "subuser-identity-section-51");
await expectStructuredWheelRow(identityDetails.getByTestId("subuser-id-51"), "Chauffør-ID", "51");
await expectStructuredWheelRow(
identityDetails.getByTestId("subuser-username-51"),
"Brugernavn",
"Brugernavn oplyses ved accept"
);
await expect(identityDetails.getByTestId("subuser-id-51")).toHaveClass(/dropdown-item-label--metadata/);
await expect(identityDetails.getByTestId("subuser-edit-name-51")).toHaveCount(0);
const systemDetails = await activateSubuserNestedSection(actions, "subuser-system-section-51");
await expectStructuredWheelRow(systemDetails.getByTestId("subuser-status-51"), "Status", "Afventer opsætning");
await expectStructuredWheelRow(systemDetails.getByTestId("subuser-created-51"), "Oprettet", "14.4.2026, 08.00.00");
await expectStructuredWheelRow(systemDetails.getByTestId("subuser-updated-51"), "Opdateret", "14.4.2026, 08.00.00");
await expect(systemDetails.getByTestId("subuser-status-51")).toHaveClass(/dropdown-item-label--metadata/);
await page.keyboard.press("Escape");
const dognvaskAccessPanel = await openSubuserAccessPanel(page, 51);
const superuserDognvaskPayloadCount = grantPayloads.length;
await dognvaskAccessPanel.getByTestId("subuser-dognvask-51-511").click();
await expect.poll(() => grantPayloads.length).toBe(superuserDognvaskPayloadCount + 1);
const superuserDognvaskPayload = grantPayloads.at(-1) || {};
expect(superuserDognvaskPayload).toEqual({
id: 511,
permissions: ["ORDERS_LIST"],
});
expect(superuserDognvaskPayload).not.toHaveProperty("permission_template_key");
await page.keyboard.press("Escape");
actions = await openSubuserActions(page, 51);
await activateSubuserActionSection(actions, "subuser-account");
await actions.getByTestId("subuser-edit-name-51").click();
await page.fill(".swal2-input", "Renamed Super Driver");
await page.getByRole("button", { name: "Gem" }).click();
await expect(page.getByTestId("subuser-name-51")).toContainText("Renamed Super Driver");
expect(profilePayloads).toContainEqual({ id: 51, payload: { name: "Renamed Super Driver" } });
actions = await openSubuserActions(page, 51);
await activateSubuserActionSection(actions, "subuser-account");
await actions.getByTestId("subuser-edit-contact-51").click();
await page.fill("#subuser-admin-email", "renamed@example.com");
await page.fill("#subuser-admin-phone-country-code", "46");
await page.fill("#subuser-admin-phone", "12312312");
await page.getByRole("button", { name: "Gem" }).click();
await expect(page.getByTestId("subuser-phone-51")).toContainText("+46 12312312");
expect(profilePayloads).toContainEqual({
id: 51,
payload: {
email: "renamed@example.com",
phone_country_code: 46,
phone: 12312312,
},
});
await expect(page.getByTestId("subuser-phone-51").getByTestId("subuser-phone-verification-51")).toContainText(
"Ikke verificeret"
);
actions = await openSubuserActions(page, 51);
await activateSubuserActionSection(actions, "subuser-contact");
const emailContact = await activateSubuserContactSection(actions, 51, "email");
await expect(emailContact).toContainText("renamed@example.com");
await expectStructuredWheelRow(
emailContact.getByTestId("subuser-action-email-verification-51"),
"Status",
"Ikke verificeret"
);
await expect(emailContact.getByTestId("subuser-action-email-verification-51").locator("select")).toBeVisible();
await expect(emailContact.getByTestId("subuser-send-email-verification-51")).toContainText("Send bekræftelsesmail");
const phoneContact = await activateSubuserContactSection(actions, 51, "phone");
await expect(phoneContact).toContainText("+46 12312312");
await expectStructuredWheelRow(
phoneContact.getByTestId("subuser-action-phone-verification-51"),
"Status",
"Ikke verificeret"
);
await expect(phoneContact.getByTestId("subuser-action-phone-verification-51").locator("select")).toBeVisible();
await expect(phoneContact.getByTestId("subuser-send-phone-verification-51")).toContainText("Send bekræftelses-SMS");
const emailContactForSend = await activateSubuserContactSection(actions, 51, "email");
await emailContactForSend.getByTestId("subuser-send-email-verification-51").click();
await expect(page.getByRole("heading", { name: "Kode sendt" })).toBeVisible();
await page.getByRole("button", { name: "Luk" }).click();
expect(verificationPayloads).toContainEqual({ id: 51, channel: "email" });
actions = await openSubuserActions(page, 51);
await activateSubuserActionSection(actions, "subuser-contact");
const emailContactForState = await activateSubuserContactSection(actions, 51, "email");
await emailContactForState.getByTestId("subuser-action-email-verification-51").locator("select").selectOption("true");
await expect.poll(() => verificationStatePayloads.length).toBe(1);
expect(verificationStatePayloads).toContainEqual({ id: 51, channel: "email", payload: { verified: true } });
actions = await openSubuserActions(page, 51);
await activateSubuserActionSection(actions, "subuser-contact");
const verifiedEmailContact = await activateSubuserContactSection(actions, 51, "email");
await expectStructuredWheelRow(
verifiedEmailContact.getByTestId("subuser-action-email-verification-51"),
"Status",
"Verificeret"
);
actions = await openSubuserActions(page, 51);
await activateSubuserActionSection(actions, "subuser-accesses");
const nordicAccessForVehicle = await activateSubuserCustomerAccess(actions, "51-510");
await (await visibleWheelItem(actions, nordicAccessForVehicle, "subuser-assign-vehicle-51-510")).click();
await expectVehicleAssignmentModalWithoutBodyScroll(page);
await page.selectOption("#subuser-assigned-vehicle", "5102");
await page.getByRole("button", { name: "Gem" }).click();
await expect(page.getByTestId("subuser-plate-51")).toHaveText("NT24680");
expect(grantPayloads).toContainEqual(expect.objectContaining({ id: 510, assigned_vehicle_id: 5102 }));
actions = await openSubuserActions(page, 51);
await activateSubuserActionSection(actions, "subuser-account");
await actions.getByTestId("subuser-password-51").click();
await page.fill("#subuser-admin-password", "DriverPass123!");
await page.fill("#subuser-admin-password-confirm", "DriverPass123!");
await page.getByRole("button", { name: "Gem" }).click();
expect(passwordPayloads).toContainEqual({ id: 51, payload: { password: "DriverPass123!" } });
actions = await openSubuserActions(page, 51);
await activateSubuserActionSection(actions, "subuser-contact");
const emailContactForLinks = await activateSubuserContactSection(actions, 51, "email");
await expect(emailContactForLinks.getByTestId("subuser-send-email-password-guide-51")).toContainText(
"Send guide til ny adgangskode"
);
await expect(emailContactForLinks.getByTestId("subuser-send-email-login-link-51")).toContainText(
"Send forhåndsgodkendt loginlink"
);
await emailContactForLinks.getByTestId("subuser-send-email-password-guide-51").click();
await expect(page.getByRole("heading", { name: "Guide sendt" })).toBeVisible();
await page.getByRole("button", { name: "Luk" }).click();
expect(passwordGuidePayloads).toContainEqual({
id: 51,
channel: "email",
payload: { customer_number: 12345678, grant_id: 510 },
});
actions = await openSubuserActions(page, 51);
await activateSubuserActionSection(actions, "subuser-contact");
const phoneContactForLinks = await activateSubuserContactSection(actions, 51, "phone");
await phoneContactForLinks.getByTestId("subuser-send-phone-login-link-51").click();
await expect(page.getByRole("heading", { name: "Loginlink sendt" })).toBeVisible();
await page.getByRole("button", { name: "Luk" }).click();
expect(loginLinkPayloads).toContainEqual({
id: 51,
channel: "phone",
payload: { customer_number: 12345678, grant_id: 510 },
});
actions = await openSubuserActions(page, 51);
await activateSubuserActionSection(actions, "subuser-accesses");
await actions.getByTestId("subuser-add-access-51").click();
await page.getByTestId("subuser-access-customer-search-input").fill("Added Customer");
await expect(page.getByTestId("subuser-access-customer-search-result-0")).toBeVisible();
await page.getByTestId("subuser-access-customer-search-result-0").click();
await expect(page.getByTestId("subuser-access-customer-search-selected")).toContainText("Added Customer");
await page.fill("#subuser-access-note", "Afløserkunde");
await page.getByRole("button", { name: "Tilføj adgang" }).click();
accessPanel = await openSubuserAccessPanel(page, 51);
await expect(accessPanel.getByTestId("subuser-access-row-51-6000")).toContainText("Added Customer");
await page.keyboard.press("Escape");
expect(addGrantPayloads).toContainEqual({
customer_number: 22222222,
subuser_id: 51,
permission_template_key: "driver",
note: "Afløserkunde",
});
await page.getByRole("button", { name: /Invit.*chauff/i }).click();
await expect(page.locator("#subuser-form-customer-number")).toBeVisible();
await page.fill("#subuser-form-customer-number", "12345678");
await page.fill("#subuser-form-name", "Super Invited Driver");
await page.fill("#subuser-form-phone-country-code", "45");
await page.fill("#subuser-form-phone", "77777777");
await page.getByRole("button", { name: "Send invitation" }).click();
await expect(page.getByRole("heading", { name: "Chauffør oprettet" })).toBeVisible();
await page.getByRole("button", { name: "Luk" }).click();
await expect(page.getByText("Super Invited Driver")).toBeVisible();
expect(invitePayloads).toContainEqual({
customer_number: 12345678,
name: "Super Invited Driver",
phone_country_code: 45,
phone: 77777777,
permission_template_key: "driver",
});
});
test("subuser self-service profile edits still save through /subusers/me", async ({ page }) => {
await primeSession(page, {
token: "subuser-profile-token",
isSubuser: true,
selectedCustomerNumber: 12345678,
});
await mockManagementApi(page, {
isSubuser: true,
});
await page.goto("/user/profile");
await expect(
page
.locator(".card-header")
.filter({ has: page.locator(".fa-user") })
.first()
).toBeVisible();
await page
.locator(".card-header")
.filter({ has: page.locator(".fa-user") })
.first()
.click();
await page.locator('button:has-text("Rediger navn")').click();
await page.fill(".swal2-input", "Profile Driver Updated");
await page.click(".swal2-confirm");
await expect(page.locator(".swal2-title")).toContainText("Navn gemt");
await page.click(".swal2-confirm");
await page
.locator(".card-header")
.filter({ has: page.locator(".fa-address-card") })
.first()
.click();
await page.locator('button:has-text("Rediger e-mail")').click();
await page.fill(".swal2-input", "profile.updated@example.com");
await page.click(".swal2-confirm");
await expect(page.locator(".swal2-title")).toContainText("E-mail gemt");
await page.click(".swal2-confirm");
});
test("subusers without SUBUSERS_LIST cannot access the chauffør page", async ({ page }) => {
await primeSession(page, {
token: "subuser-no-access-token",
isSubuser: true,
selectedCustomerNumber: 12345678,
});
await mockManagementApi(page, {
isSubuser: true,
subuserPermissions: ["BOOKINGS_LIST"],
});
await page.goto("/user/subusers");
const forbidden = page.getByTestId("restricted-forbidden");
await expect(forbidden).toBeVisible();
await expect(forbidden.getByRole("heading", { name: "403" })).toBeVisible();
});