Use the stable localized forbidden-page contract in customer, subuser, and superuser denial tests while retaining the protected-action and zero-request assertions.
1932 lines
74 KiB
TypeScript
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();
|
|
});
|