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