1251 lines
45 KiB
TypeScript
1251 lines
45 KiB
TypeScript
import { expect, test, type Page } from "@playwright/test";
|
|
import { API_HOST, seedAuthenticatedState } from "./support/network.js";
|
|
import { isDesktopProject } from "./support/projects";
|
|
|
|
const json = (body: unknown, status = 200) => ({
|
|
status,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
const limitedManagerPermissions = [
|
|
"user",
|
|
"limited_backoffice_access",
|
|
"limited_backoffice_prices_manage",
|
|
"limited_backoffice_customer_pricing_view",
|
|
"limited_backoffice_customer_pricing_manage",
|
|
"limited_backoffice_employees_manage",
|
|
"department_access_1",
|
|
"department_access_2",
|
|
];
|
|
|
|
const sessionData = {
|
|
id: 50,
|
|
customer_number: 12345,
|
|
group_id: 77,
|
|
email: "manager@example.com",
|
|
phone: {
|
|
number: "12345678",
|
|
country_code: 45,
|
|
},
|
|
notifications: {
|
|
wash_certificate_email: null,
|
|
email_notifications_enabled: true,
|
|
sms_notifications_enabled: false,
|
|
},
|
|
created_at: "2026-01-01T00:00:00.000Z",
|
|
updated_at: "2026-01-01T00:00:00.000Z",
|
|
display_name: "Limited Manager",
|
|
permissions: limitedManagerPermissions,
|
|
economic_customer: [],
|
|
runtime_config: {
|
|
economic: {
|
|
transaction_draft_customer_number: null,
|
|
default_distribution_department_id: null,
|
|
},
|
|
release: {},
|
|
},
|
|
};
|
|
|
|
const superuserSessionData = {
|
|
...sessionData,
|
|
id: 51,
|
|
customer_number: 12346,
|
|
email: "superuser@example.com",
|
|
display_name: "Superuser",
|
|
permissions: ["user", "superuser", "limited_backoffice_access", "department_access_1"],
|
|
};
|
|
|
|
const adminLimitedSessionData = {
|
|
...sessionData,
|
|
id: 52,
|
|
customer_number: 12347,
|
|
email: "admin-manager@example.com",
|
|
display_name: "Admin Manager",
|
|
permissions: ["user", "admin", "limited_backoffice_access", "department_access_1"],
|
|
};
|
|
|
|
const assignedDepartments = [
|
|
{ id: 1, name: "Assigned Depot", description: "", visible: true, archived: false, custom_pricing_only: true },
|
|
{ id: 2, name: "Remote Depot", description: "", visible: true, archived: false, custom_pricing_only: false },
|
|
];
|
|
|
|
const pricePayload = {
|
|
department: { id: 1, name: "Assigned Depot", description: "" },
|
|
categories: [
|
|
{
|
|
id: 10,
|
|
name: "Exterior",
|
|
description: "",
|
|
products: [
|
|
{
|
|
id: 101,
|
|
name: "Truck wash",
|
|
description: "Standard wash",
|
|
price: 125,
|
|
default_price: 8742,
|
|
},
|
|
{
|
|
id: 102,
|
|
name: "Trailer wash",
|
|
description: "",
|
|
price: 95,
|
|
default_price: 9842,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const duplicatePricePayload = {
|
|
...pricePayload,
|
|
categories: [
|
|
{
|
|
...pricePayload.categories[0],
|
|
products: [
|
|
pricePayload.categories[0].products[0],
|
|
{ ...pricePayload.categories[0].products[0], price: 999 },
|
|
pricePayload.categories[0].products[1],
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const cloneJson = <T>(value: T): T => JSON.parse(JSON.stringify(value));
|
|
|
|
const applyPriceRowsToPayload = (
|
|
payload: typeof pricePayload,
|
|
priceRows: Array<{ product_id: number; price: number | string }>
|
|
) => {
|
|
const updatedPayload = cloneJson(payload);
|
|
const priceLookup = new Map(priceRows.map((row) => [Number(row.product_id), Number(row.price)]));
|
|
|
|
for (const category of updatedPayload.categories) {
|
|
for (const product of category.products) {
|
|
if (priceLookup.has(Number(product.id))) {
|
|
product.price = priceLookup.get(Number(product.id)) ?? product.price;
|
|
}
|
|
}
|
|
}
|
|
|
|
return updatedPayload;
|
|
};
|
|
|
|
const deferred = () => {
|
|
let resolve!: () => void;
|
|
const promise = new Promise<void>((resolvePromise) => {
|
|
resolve = resolvePromise;
|
|
});
|
|
|
|
return { promise, resolve };
|
|
};
|
|
|
|
const rolePermissionGroups = {
|
|
viewer: [{ key: "account", capabilities: ["sign_in", "view_own_permissions"] }],
|
|
cashier: [
|
|
{ key: "account", capabilities: ["sign_in", "view_own_permissions"] },
|
|
{
|
|
key: "orders",
|
|
capabilities: [
|
|
"view_orders",
|
|
"create_orders",
|
|
"edit_orders",
|
|
"complete_orders",
|
|
"view_order_items",
|
|
"create_order_items",
|
|
"update_order_lines",
|
|
"remove_order_lines",
|
|
"charge_orders",
|
|
],
|
|
},
|
|
{
|
|
key: "products",
|
|
capabilities: ["view_product_catalog", "view_product_recommendations"],
|
|
},
|
|
{
|
|
key: "customers",
|
|
capabilities: [
|
|
"search_customers",
|
|
"view_customer_details",
|
|
"view_customer_notes",
|
|
"add_customer_notes",
|
|
"view_customer_flags",
|
|
],
|
|
},
|
|
{
|
|
key: "vehicles",
|
|
capabilities: ["search_vehicles", "view_vehicle_matches", "view_vehicle_history"],
|
|
},
|
|
{
|
|
key: "attachments",
|
|
capabilities: ["view_order_attachments", "add_order_attachments", "download_order_attachments"],
|
|
},
|
|
{
|
|
key: "scanner",
|
|
capabilities: ["view_plate_scans"],
|
|
},
|
|
{
|
|
key: "bookings",
|
|
capabilities: [
|
|
"view_department_bookings",
|
|
"view_own_bookings",
|
|
"update_bookings",
|
|
"create_bookings",
|
|
"mark_bookings_complete",
|
|
"send_booking_confirmations",
|
|
],
|
|
},
|
|
],
|
|
booking_coordinator: [
|
|
{ key: "account", capabilities: ["sign_in", "view_own_permissions"] },
|
|
{ key: "orders", capabilities: ["view_orders"] },
|
|
{
|
|
key: "bookings",
|
|
capabilities: [
|
|
"view_department_bookings",
|
|
"view_own_bookings",
|
|
"update_bookings",
|
|
"create_bookings",
|
|
"mark_bookings_complete",
|
|
"send_booking_confirmations",
|
|
],
|
|
},
|
|
{
|
|
key: "time_bookings",
|
|
capabilities: ["view_time_booking_entries", "create_time_booking_entries", "edit_time_booking_entries"],
|
|
},
|
|
],
|
|
operations_lead: [
|
|
{ key: "account", capabilities: ["sign_in", "view_own_permissions"] },
|
|
{
|
|
key: "orders",
|
|
capabilities: [
|
|
"view_orders",
|
|
"create_orders",
|
|
"edit_orders",
|
|
"delete_orders",
|
|
"complete_orders",
|
|
"view_order_items",
|
|
"create_order_items",
|
|
"update_order_lines",
|
|
"remove_order_lines",
|
|
"charge_orders",
|
|
],
|
|
},
|
|
{
|
|
key: "products",
|
|
capabilities: ["view_product_catalog", "view_product_recommendations"],
|
|
},
|
|
{
|
|
key: "customers",
|
|
capabilities: [
|
|
"search_customers",
|
|
"view_customer_details",
|
|
"view_customer_notes",
|
|
"add_customer_notes",
|
|
"view_customer_flags",
|
|
],
|
|
},
|
|
{
|
|
key: "vehicles",
|
|
capabilities: ["search_vehicles", "view_vehicle_matches", "view_vehicle_history"],
|
|
},
|
|
{
|
|
key: "attachments",
|
|
capabilities: ["view_order_attachments", "add_order_attachments", "download_order_attachments"],
|
|
},
|
|
{
|
|
key: "scanner",
|
|
capabilities: ["view_plate_scans"],
|
|
},
|
|
{
|
|
key: "bookings",
|
|
capabilities: [
|
|
"view_department_bookings",
|
|
"view_own_bookings",
|
|
"update_bookings",
|
|
"create_bookings",
|
|
"mark_bookings_complete",
|
|
"send_booking_confirmations",
|
|
],
|
|
},
|
|
{ key: "reports", capabilities: ["view_order_statistics", "view_booking_statistics"] },
|
|
],
|
|
department_admin: [
|
|
{ key: "account", capabilities: ["sign_in", "view_own_permissions"] },
|
|
{
|
|
key: "orders",
|
|
capabilities: [
|
|
"view_orders",
|
|
"create_orders",
|
|
"edit_orders",
|
|
"delete_orders",
|
|
"complete_orders",
|
|
"view_order_items",
|
|
"create_order_items",
|
|
"update_order_lines",
|
|
"remove_order_lines",
|
|
"charge_orders",
|
|
],
|
|
},
|
|
{
|
|
key: "products",
|
|
capabilities: ["view_product_catalog", "view_product_recommendations"],
|
|
},
|
|
{
|
|
key: "customers",
|
|
capabilities: [
|
|
"search_customers",
|
|
"view_customer_details",
|
|
"view_customer_notes",
|
|
"add_customer_notes",
|
|
"view_customer_flags",
|
|
],
|
|
},
|
|
{
|
|
key: "vehicles",
|
|
capabilities: ["search_vehicles", "view_vehicle_matches", "view_vehicle_history"],
|
|
},
|
|
{
|
|
key: "attachments",
|
|
capabilities: ["view_order_attachments", "add_order_attachments", "download_order_attachments"],
|
|
},
|
|
{
|
|
key: "scanner",
|
|
capabilities: ["view_plate_scans"],
|
|
},
|
|
{
|
|
key: "bookings",
|
|
capabilities: [
|
|
"view_department_bookings",
|
|
"view_own_bookings",
|
|
"update_bookings",
|
|
"create_bookings",
|
|
"mark_bookings_complete",
|
|
"send_booking_confirmations",
|
|
],
|
|
},
|
|
{ key: "reports", capabilities: ["view_order_statistics", "view_booking_statistics"] },
|
|
{
|
|
key: "limited_backoffice",
|
|
capabilities: [
|
|
"open_limited_backoffice",
|
|
"manage_department_prices",
|
|
"view_customer_pricing",
|
|
"manage_customer_pricing",
|
|
"manage_employee_access",
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const remotePricePayload = {
|
|
...pricePayload,
|
|
department: { id: 2, name: "Remote Depot", description: "" },
|
|
};
|
|
|
|
const customerPricingPayload = (overrides: Array<Record<string, unknown>> = []) => ({
|
|
department: { id: 1, name: "Assigned Depot", description: "", custom_pricing_only: true },
|
|
customer: { id: 601, customer_number: 6001, display_name: "Acme Haulage" },
|
|
overrides,
|
|
categories: [
|
|
{
|
|
id: 10,
|
|
name: "Exterior",
|
|
description: "",
|
|
products: [
|
|
{
|
|
id: 101,
|
|
name: "Truck wash",
|
|
description: "Standard wash",
|
|
category: 10,
|
|
apply_category_discount: true,
|
|
base_price: 8742,
|
|
department_price: 125,
|
|
effective_price: overrides.length ? 100 : 125,
|
|
missing_department_price: false,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
const rolesPayload = [
|
|
{
|
|
key: "viewer",
|
|
label: "Deactivated",
|
|
description: "No order, booking, or management access.",
|
|
permission_groups: rolePermissionGroups.viewer,
|
|
},
|
|
{ key: "cashier", label: "Cashier", description: "Can sell.", permission_groups: rolePermissionGroups.cashier },
|
|
{
|
|
key: "booking_coordinator",
|
|
label: "Booking coordinator",
|
|
description: "Can coordinate.",
|
|
permission_groups: rolePermissionGroups.booking_coordinator,
|
|
},
|
|
{
|
|
key: "operations_lead",
|
|
label: "Operations lead",
|
|
description: "Can coordinate operations.",
|
|
permission_groups: rolePermissionGroups.operations_lead,
|
|
},
|
|
{
|
|
key: "department_admin",
|
|
label: "Department admin",
|
|
description: "Can administer.",
|
|
permission_groups: rolePermissionGroups.department_admin,
|
|
},
|
|
{ key: "superuser", label: "Superuser", description: "Must not render." },
|
|
];
|
|
|
|
const employeesPayload = [
|
|
{
|
|
id: 501,
|
|
user_id: 501,
|
|
customer_number: 0,
|
|
display_name: "Casey Clerk",
|
|
email: "casey@example.com",
|
|
phone_country_code: 45,
|
|
phone: 12345678,
|
|
active: true,
|
|
role: { key: "cashier", label: "Cashier", description: "Can sell." },
|
|
departments: [{ id: 1, name: "Assigned Depot" }],
|
|
created_at: "2026-01-01 00:00:00",
|
|
updated_at: "2026-01-01 00:00:00",
|
|
},
|
|
{
|
|
id: 502,
|
|
customer_number: 0,
|
|
display_name: "Riley Remote",
|
|
email: "riley@example.com",
|
|
active: true,
|
|
role: { key: "viewer", label: "Deactivated", description: "No order, booking, or management access." },
|
|
departments: [{ id: 2, name: "Remote Depot" }],
|
|
created_at: "2026-01-01 00:00:00",
|
|
updated_at: "2026-01-01 00:00:00",
|
|
},
|
|
];
|
|
|
|
async function seedLimitedBackofficeSession(page, token = "limited-backoffice-token") {
|
|
await page.addInitScript(() => {
|
|
window.localStorage.setItem("locale", "en");
|
|
});
|
|
await seedAuthenticatedState(page, token);
|
|
}
|
|
|
|
async function mockLimitedBackofficeApi(page, authSessionData = sessionData, options: any = {}) {
|
|
const calls: string[] = [];
|
|
const forbiddenCalls: string[] = [];
|
|
const priceUpdateCalls: unknown[] = [];
|
|
const customerPricingUpdateCalls: unknown[] = [];
|
|
const employeeCreateCalls: unknown[] = [];
|
|
const employeeUpdateCalls: unknown[] = [];
|
|
const loginLinkCalls: string[] = [];
|
|
const currentPricePayload = {
|
|
value: cloneJson(options.pricePayload ?? pricePayload),
|
|
};
|
|
const currentEmployees = {
|
|
value: cloneJson(options.employeesPayload ?? employeesPayload),
|
|
};
|
|
|
|
await page.route(API_HOST, async (route) => {
|
|
const request = route.request();
|
|
const url = new URL(request.url());
|
|
const pathname = url.pathname;
|
|
const method = request.method();
|
|
calls.push(`${method} ${pathname}`);
|
|
|
|
if (pathname.endsWith("/products") || pathname.endsWith("/superuser/department/prices")) {
|
|
forbiddenCalls.push(`${method} ${pathname}`);
|
|
await route.fulfill(json({ message: "Forbidden endpoint called" }, 599));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/auth/session") && method === "GET") {
|
|
await route.fulfill(json({ data: authSessionData }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/ping") && method === "GET") {
|
|
await route.fulfill(json({ data: { ok: true } }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/worker/version") && method === "GET") {
|
|
await route.fulfill(json({ data: { version: "limited-backoffice-test" } }));
|
|
return;
|
|
}
|
|
|
|
if (
|
|
(pathname.endsWith("/auth/recaptcha/pre-check") || pathname.endsWith("/auth/reCAPTCHA/public")) &&
|
|
method === "GET"
|
|
) {
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
recaptcha: { enabled: false, site_key: "" },
|
|
rate_limit: { enabled: false, limit: 0, remaining: 0, reset: 0, warning: null },
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/limited-backoffice/departments") && method === "GET") {
|
|
await route.fulfill(json({ data: assignedDepartments }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/limited-backoffice/departments/1/prices") && method === "GET") {
|
|
await route.fulfill(json({ data: currentPricePayload.value }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/limited-backoffice/departments/2/prices") && method === "GET") {
|
|
await route.fulfill(json({ data: remotePricePayload }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/limited-backoffice/departments/1/customer-pricing") && method === "GET") {
|
|
await route.fulfill(json({ data: customerPricingPayload() }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/limited-backoffice/departments/1/customer-pricing") && method === "PUT") {
|
|
const body = request.postDataJSON?.() || null;
|
|
customerPricingUpdateCalls.push(body);
|
|
await route.fulfill(
|
|
json({
|
|
data: customerPricingPayload([
|
|
{
|
|
is_category: false,
|
|
product_or_category_id: 101,
|
|
percentage: 20,
|
|
fixed_price: null,
|
|
},
|
|
]),
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/limited-backoffice/departments/1/prices") && method === "PUT") {
|
|
const body = request.postDataJSON?.() || null;
|
|
priceUpdateCalls.push(body);
|
|
|
|
if (typeof options.onPriceUpdate === "function") {
|
|
await options.onPriceUpdate({
|
|
route,
|
|
request,
|
|
body,
|
|
currentPricePayload,
|
|
});
|
|
return;
|
|
}
|
|
|
|
currentPricePayload.value = applyPriceRowsToPayload(currentPricePayload.value, body?.prices || []);
|
|
await route.fulfill(json({ data: currentPricePayload.value }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/limited-backoffice/roles") && method === "GET") {
|
|
await route.fulfill(json({ data: rolesPayload }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/limited-backoffice/employees") && method === "GET") {
|
|
await route.fulfill(json({ data: currentEmployees.value }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/limited-backoffice/employees") && method === "POST") {
|
|
const body = request.postDataJSON?.() || {};
|
|
employeeCreateCalls.push(body);
|
|
const role = rolesPayload.find((item) => item.key === body.role_key) || rolesPayload[0];
|
|
const created = {
|
|
id: 900 + currentEmployees.value.length + 1,
|
|
customer_number: 900001000 + currentEmployees.value.length + 1,
|
|
display_name: body.display_name,
|
|
email: body.email,
|
|
phone_country_code: body.phone_country_code ?? null,
|
|
phone: body.phone ?? null,
|
|
active: true,
|
|
role,
|
|
departments: assignedDepartments.filter((department) => body.department_ids?.includes(department.id)),
|
|
created_at: "2026-01-01 00:00:00",
|
|
updated_at: "2026-01-01 00:00:00",
|
|
};
|
|
currentEmployees.value.unshift(created);
|
|
await route.fulfill(json({ data: created }));
|
|
return;
|
|
}
|
|
|
|
const loginLinkMatch = pathname.match(/\/limited-backoffice\/employees\/(\d+)\/login-link$/);
|
|
if (loginLinkMatch && method === "POST") {
|
|
const employeeId = loginLinkMatch[1];
|
|
const status = options.loginLinkStatus ?? 200;
|
|
loginLinkCalls.push(employeeId);
|
|
await route.fulfill(
|
|
json(
|
|
status === 200
|
|
? {
|
|
data: {
|
|
employee_id: Number(employeeId),
|
|
login_path: `/login/qr?token=${"a".repeat(64)}`,
|
|
},
|
|
}
|
|
: { message: options.loginLinkMessage ?? "Could not create employee login link." },
|
|
status
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
const employeeMatch = pathname.match(/\/limited-backoffice\/employees\/(\d+)$/);
|
|
if (employeeMatch && method === "PUT") {
|
|
const body = request.postDataJSON?.() || {};
|
|
employeeUpdateCalls.push(body);
|
|
const employeeId = Number(employeeMatch[1]);
|
|
const target = currentEmployees.value.find((employee) => Number(employee.id) === employeeId);
|
|
if (!target) {
|
|
await route.fulfill(json({ message: "Employee not found" }, 404));
|
|
return;
|
|
}
|
|
|
|
const role = rolesPayload.find((item) => item.key === body.role_key) || target.role;
|
|
Object.assign(target, {
|
|
display_name: body.display_name ?? target.display_name,
|
|
email: body.email ?? target.email,
|
|
phone_country_code: body.phone_country_code ?? null,
|
|
phone: body.phone ?? null,
|
|
role,
|
|
departments: Array.isArray(body.department_ids)
|
|
? assignedDepartments.filter((department) => body.department_ids.includes(department.id))
|
|
: target.departments,
|
|
updated_at: "2026-01-01 01:00:00",
|
|
});
|
|
await route.fulfill(json({ data: target }));
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(json({ data: [] }));
|
|
});
|
|
|
|
return {
|
|
calls,
|
|
forbiddenCalls,
|
|
priceUpdateCalls,
|
|
customerPricingUpdateCalls,
|
|
employeeCreateCalls,
|
|
employeeUpdateCalls,
|
|
loginLinkCalls,
|
|
};
|
|
}
|
|
|
|
async function expectDepartmentSelectorInTitleRow(page: Page) {
|
|
const headingBox = await page.getByRole("heading", { name: "Backoffice" }).boundingBox();
|
|
const selectBox = await page.getByTestId("limited-backoffice-department-select").boundingBox();
|
|
|
|
expect(headingBox).not.toBeNull();
|
|
expect(selectBox).not.toBeNull();
|
|
if (!headingBox || !selectBox) {
|
|
return;
|
|
}
|
|
|
|
expect(selectBox.x).toBeGreaterThan(headingBox.x);
|
|
expect(Math.abs(selectBox.y + selectBox.height / 2 - (headingBox.y + headingBox.height / 2))).toBeLessThan(48);
|
|
}
|
|
|
|
test.describe("Limited backoffice", () => {
|
|
test("shows the limited backoffice header shortcut only on department-scoped admin pages", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
test.skip(!isDesktopProject(testInfo), "Desktop only");
|
|
|
|
await seedLimitedBackofficeSession(page);
|
|
await mockLimitedBackofficeApi(page);
|
|
|
|
await page.goto("/admin/1");
|
|
|
|
const desktopNavigation = page.getByTestId("desktop-buefy-navigation");
|
|
const backofficeNavItem = desktopNavigation.locator('a[href="/backoffice"]:visible');
|
|
const headerShortcut = page.getByTestId("limited-backoffice-header-button");
|
|
|
|
await expect(backofficeNavItem).toHaveCount(0);
|
|
await expect(headerShortcut).toBeVisible();
|
|
await expect(headerShortcut).toHaveAttribute("href", "/backoffice/departments/1/prices");
|
|
await expect(headerShortcut).toContainText("Backoffice");
|
|
await expect(headerShortcut.locator(".icon i.fas.fa-tools")).toHaveCount(1);
|
|
|
|
await page.goto("/admin");
|
|
|
|
await expect(page.getByTestId("limited-backoffice-header-button")).toHaveCount(0);
|
|
});
|
|
|
|
test("hides the limited backoffice header shortcut for superusers", async ({ page }, testInfo) => {
|
|
test.skip(!isDesktopProject(testInfo), "Desktop only");
|
|
|
|
await seedLimitedBackofficeSession(page, "limited-backoffice-superuser-token");
|
|
await mockLimitedBackofficeApi(page, superuserSessionData);
|
|
|
|
await page.goto("/admin/1");
|
|
|
|
await expect(page.getByTestId("desktop-buefy-navigation").locator('a[href="/backoffice"]:visible')).toHaveCount(0);
|
|
await expect(page.getByTestId("limited-backoffice-header-button")).toHaveCount(0);
|
|
await expect(page.getByTestId("superuser-backoffice-header-button")).toBeVisible();
|
|
});
|
|
|
|
test("links backoffice department pages to the matching admin department for admin-capable users", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
test.skip(!isDesktopProject(testInfo), "Desktop only");
|
|
|
|
await seedLimitedBackofficeSession(page, "admin-limited-backoffice-token");
|
|
await mockLimitedBackofficeApi(page, adminLimitedSessionData);
|
|
|
|
await page.goto("/backoffice/departments/1/prices");
|
|
|
|
const adminShortcut = page.getByTestId("admin-header-button");
|
|
|
|
await expect(adminShortcut).toBeVisible();
|
|
await expect(adminShortcut).toHaveAttribute("href", "/admin/1");
|
|
await expect(adminShortcut).toContainText("Departments");
|
|
await expect(page.getByTestId("limited-backoffice-header-button")).toHaveCount(0);
|
|
});
|
|
|
|
test("does not show the admin shortcut on backoffice department pages for limited-only users", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
test.skip(!isDesktopProject(testInfo), "Desktop only");
|
|
|
|
await seedLimitedBackofficeSession(page);
|
|
await mockLimitedBackofficeApi(page);
|
|
|
|
await page.goto("/backoffice/departments/1/prices");
|
|
|
|
await expect(page.getByTestId("admin-header-button")).toHaveCount(0);
|
|
});
|
|
|
|
test("limits price management to assigned departments and explicit prices", async ({ page }) => {
|
|
await seedLimitedBackofficeSession(page);
|
|
const api = await mockLimitedBackofficeApi(page);
|
|
|
|
await page.goto("/backoffice/departments/1/prices");
|
|
|
|
await expect(page.getByTestId("limited-prices-title")).toBeVisible();
|
|
await expectDepartmentSelectorInTitleRow(page);
|
|
await expect(page.getByTestId("limited-backoffice-department-select")).toContainText("Assigned Depot");
|
|
await expect(page.getByTestId("limited-backoffice-department-select")).toContainText("Remote Depot");
|
|
await expect(page.getByTestId("limited-backoffice-department-select")).not.toContainText("Other Depot");
|
|
await expect(page.getByTestId("limited-price-row-101")).toContainText("Truck wash");
|
|
await expect(page.getByTestId("limited-price-input-101")).toHaveValue("125");
|
|
|
|
await expect(page.locator("body")).not.toContainText("8742");
|
|
await expect(page.locator("body")).not.toContainText(/default price/i);
|
|
await expect(page.locator("body")).not.toContainText("department_access_1");
|
|
await expect(page.locator("body")).not.toContainText("limited_backoffice_prices_manage");
|
|
|
|
await expect(page.getByTestId("limited-prices-save")).toHaveCount(0);
|
|
await expect(page.getByTestId("limited-prices-autosave-status")).toContainText(/saved/i);
|
|
await page.getByTestId("limited-price-input-101").fill("");
|
|
await expect(page.getByTestId("limited-price-validation")).toBeVisible();
|
|
await page.waitForTimeout(700);
|
|
|
|
expect(api.priceUpdateCalls).toEqual([]);
|
|
expect(api.forbiddenCalls).toEqual([]);
|
|
});
|
|
|
|
test("shows customer pricing only for custom-only departments and saves scoped discounts", async ({ page }) => {
|
|
await seedLimitedBackofficeSession(page);
|
|
const api = await mockLimitedBackofficeApi(page);
|
|
|
|
await page.goto("/backoffice/departments/1/customer-pricing");
|
|
|
|
await expect(page.getByTestId("limited-backoffice-tab-customer-pricing")).toBeVisible();
|
|
await page.getByTestId("department-customer-pricing-customer-number").fill("6001");
|
|
await page.getByTestId("department-customer-pricing-load").click();
|
|
|
|
await expect(page.getByTestId("department-customer-pricing-customer")).toContainText("Acme Haulage");
|
|
await expect(page.getByTestId("department-customer-pricing-product-101")).toContainText("Truck wash");
|
|
|
|
await page.getByTestId("department-customer-pricing-item-discount-101").click();
|
|
await page.locator(".swal2-input").fill("20");
|
|
await page.locator(".swal2-confirm").click();
|
|
|
|
await expect(page.getByTestId("department-customer-pricing-item-discount-101")).toContainText("20%");
|
|
expect(api.customerPricingUpdateCalls).toEqual([
|
|
{
|
|
customer_number: 6001,
|
|
overrides: [
|
|
{
|
|
is_category: false,
|
|
product_or_category_id: 101,
|
|
percentage: 20,
|
|
fixed_price: null,
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
await page.getByTestId("limited-backoffice-department-select").selectOption("2");
|
|
await expect(page).toHaveURL(/\/backoffice\/departments\/2\/customer-pricing$/);
|
|
await expect(page.getByTestId("limited-backoffice-tab-customer-pricing")).toHaveCount(0);
|
|
await expect(page.getByTestId("department-customer-pricing-disabled")).toBeVisible();
|
|
});
|
|
|
|
test("autosaves valid price changes after a typing pause without blur", async ({ page }) => {
|
|
await seedLimitedBackofficeSession(page);
|
|
const api = await mockLimitedBackofficeApi(page);
|
|
|
|
await page.goto("/backoffice/departments/1/prices");
|
|
|
|
const priceInput = page.getByTestId("limited-price-input-101");
|
|
await expect(priceInput).toHaveValue("125");
|
|
|
|
await priceInput.fill("130");
|
|
|
|
await expect(page.getByTestId("limited-prices-autosave-status")).toContainText(/pending/i);
|
|
await expect.poll(() => api.priceUpdateCalls.length).toBe(1);
|
|
await expect(priceInput).toBeFocused();
|
|
expect(api.priceUpdateCalls[0]).toEqual({
|
|
prices: [
|
|
{ product_id: 101, price: "130" },
|
|
{ product_id: 102, price: "95" },
|
|
],
|
|
});
|
|
await expect(page.getByTestId("limited-prices-autosave-status")).toContainText(/saved/i);
|
|
});
|
|
|
|
test("flushes a valid pending price change on blur", async ({ page }) => {
|
|
await seedLimitedBackofficeSession(page);
|
|
const api = await mockLimitedBackofficeApi(page);
|
|
|
|
await page.goto("/backoffice/departments/1/prices");
|
|
|
|
const priceInput = page.getByTestId("limited-price-input-101");
|
|
await priceInput.fill("133");
|
|
await priceInput.blur();
|
|
|
|
await expect.poll(() => api.priceUpdateCalls.length).toBe(1);
|
|
expect(api.priceUpdateCalls[0]).toEqual({
|
|
prices: [
|
|
{ product_id: 101, price: "133" },
|
|
{ product_id: 102, price: "95" },
|
|
],
|
|
});
|
|
});
|
|
|
|
test("keeps newer edits when an older autosave response resolves late", async ({ page }) => {
|
|
const firstSave = deferred();
|
|
let saveCount = 0;
|
|
|
|
await seedLimitedBackofficeSession(page);
|
|
const api = await mockLimitedBackofficeApi(page, sessionData, {
|
|
onPriceUpdate: async ({ route, body, currentPricePayload }) => {
|
|
saveCount += 1;
|
|
|
|
if (saveCount === 1) {
|
|
await firstSave.promise;
|
|
await route.fulfill(json({ data: pricePayload }));
|
|
return;
|
|
}
|
|
|
|
currentPricePayload.value = applyPriceRowsToPayload(currentPricePayload.value, body?.prices || []);
|
|
await route.fulfill(json({ data: currentPricePayload.value }));
|
|
},
|
|
});
|
|
|
|
await page.goto("/backoffice/departments/1/prices");
|
|
|
|
const priceInput = page.getByTestId("limited-price-input-101");
|
|
await priceInput.fill("130");
|
|
await expect.poll(() => api.priceUpdateCalls.length).toBe(1);
|
|
|
|
await priceInput.fill("140");
|
|
await page.waitForTimeout(700);
|
|
expect(api.priceUpdateCalls.length).toBe(1);
|
|
|
|
firstSave.resolve();
|
|
|
|
await expect.poll(() => api.priceUpdateCalls.length).toBe(2);
|
|
await expect(priceInput).toHaveValue("140");
|
|
expect(api.priceUpdateCalls[1]).toEqual({
|
|
prices: [
|
|
{ product_id: 101, price: "140" },
|
|
{ product_id: 102, price: "95" },
|
|
],
|
|
});
|
|
});
|
|
|
|
test("keeps failed autosaves dirty and retries on the next valid edit", async ({ page }) => {
|
|
let shouldFail = true;
|
|
|
|
await seedLimitedBackofficeSession(page);
|
|
const api = await mockLimitedBackofficeApi(page, sessionData, {
|
|
onPriceUpdate: async ({ route, body, currentPricePayload }) => {
|
|
if (shouldFail) {
|
|
shouldFail = false;
|
|
await route.fulfill(json({ message: "Price save failed" }, 422));
|
|
return;
|
|
}
|
|
|
|
currentPricePayload.value = applyPriceRowsToPayload(currentPricePayload.value, body?.prices || []);
|
|
await route.fulfill(json({ data: currentPricePayload.value }));
|
|
},
|
|
});
|
|
|
|
await page.goto("/backoffice/departments/1/prices");
|
|
|
|
const priceInput = page.getByTestId("limited-price-input-101");
|
|
await priceInput.fill("130");
|
|
await expect.poll(() => api.priceUpdateCalls.length).toBe(1);
|
|
await expect(page.getByTestId("limited-prices-error")).toContainText("Price save failed");
|
|
await expect(page.getByTestId("limited-prices-autosave-status")).toContainText(/pending/i);
|
|
|
|
await priceInput.fill("131");
|
|
|
|
await expect.poll(() => api.priceUpdateCalls.length).toBe(2);
|
|
await expect(page.getByTestId("limited-prices-error")).toHaveCount(0);
|
|
await expect(page.getByTestId("limited-prices-autosave-status")).toContainText(/saved/i);
|
|
expect(api.priceUpdateCalls[1]).toEqual({
|
|
prices: [
|
|
{ product_id: 101, price: "131" },
|
|
{ product_id: 102, price: "95" },
|
|
],
|
|
});
|
|
});
|
|
|
|
test("deduplicates repeated products in the price configuration", async ({ page }, testInfo) => {
|
|
test.skip(!isDesktopProject(testInfo), "Desktop only");
|
|
|
|
await seedLimitedBackofficeSession(page);
|
|
const api = await mockLimitedBackofficeApi(page, sessionData, { pricePayload: duplicatePricePayload });
|
|
|
|
await page.goto("/backoffice/departments/1/prices");
|
|
|
|
await expect(page.getByTestId("limited-price-row-101")).toHaveCount(1);
|
|
await expect(page.getByTestId("limited-price-row-102")).toHaveCount(1);
|
|
await expect(page.getByTestId("limited-price-input-101")).toHaveValue("125");
|
|
|
|
await page.getByTestId("limited-price-input-101").fill("130");
|
|
|
|
await expect.poll(() => api.priceUpdateCalls.length).toBe(1);
|
|
expect(api.priceUpdateCalls[0]).toEqual({
|
|
prices: [
|
|
{ product_id: 101, price: "130" },
|
|
{ product_id: 102, price: "95" },
|
|
],
|
|
});
|
|
expect(api.priceUpdateCalls[0]).not.toEqual({
|
|
prices: [
|
|
{ product_id: 101, price: "130" },
|
|
{ product_id: 101, price: "130" },
|
|
{ product_id: 102, price: "95" },
|
|
],
|
|
});
|
|
expect(api.forbiddenCalls).toEqual([]);
|
|
});
|
|
|
|
test("shows only limited role presets in employee access", async ({ page }, testInfo) => {
|
|
test.skip(!isDesktopProject(testInfo), "Desktop only");
|
|
|
|
await seedLimitedBackofficeSession(page);
|
|
const api = await mockLimitedBackofficeApi(page);
|
|
|
|
await page.goto("/backoffice/departments/1/employees");
|
|
|
|
await expect(page.getByTestId("limited-employees-title")).toBeVisible();
|
|
await expectDepartmentSelectorInTitleRow(page);
|
|
await expect(page.getByTestId("limited-employee-row-501")).toContainText("Casey Clerk");
|
|
await expect(page.getByTestId("limited-employee-user-id-501")).toHaveText("501");
|
|
await expect(page.getByTestId("limited-employee-phone-501")).toHaveText("+45 12345678");
|
|
await expect(page.getByTestId("limited-employee-row-502")).toHaveCount(0);
|
|
await expect(page.getByTestId("limited-employee-department-1")).toBeChecked();
|
|
await expect(page.getByTestId("limited-employee-department-2")).not.toBeChecked();
|
|
await expect(page.locator("#limited-employee-role option")).toHaveCount(5);
|
|
await expect(page.getByTestId("limited-employee-role")).not.toContainText("Superuser");
|
|
await expect(page.locator("body")).not.toContainText("department_access_1");
|
|
await expect(page.locator("body")).not.toContainText("limited_backoffice_employees_manage");
|
|
await expect(page.locator("body")).not.toContainText("raw_permissions");
|
|
|
|
expect(api.forbiddenCalls).toEqual([]);
|
|
});
|
|
|
|
test("generates an employee QR login link", async ({ page }, testInfo) => {
|
|
test.skip(!isDesktopProject(testInfo), "Desktop only");
|
|
|
|
await seedLimitedBackofficeSession(page);
|
|
const api = await mockLimitedBackofficeApi(page);
|
|
|
|
await page.goto("/backoffice/employees");
|
|
|
|
await page.getByTestId("limited-employee-login-link-501").click();
|
|
|
|
const modal = page.getByTestId("limited-login-link-modal");
|
|
await expect(modal).toBeVisible();
|
|
await expect(modal).toContainText("Employee login link");
|
|
await expect(modal).toContainText("Casey Clerk");
|
|
await expect(page.getByTestId("limited-login-link-qr")).toHaveAttribute("src", /^data:image\/png;base64,/);
|
|
await expect(page.getByTestId("limited-login-link-url")).toHaveValue(/\/login\/qr\?token=a{64}$/);
|
|
|
|
await page.getByTestId("limited-login-link-copy").click();
|
|
|
|
await expect(page.getByTestId("limited-login-link-copy-message")).toHaveText("Link copied.");
|
|
expect(api.loginLinkCalls).toEqual(["501"]);
|
|
expect(api.forbiddenCalls).toEqual([]);
|
|
});
|
|
|
|
test("shows an error when employee QR login link generation fails", async ({ page }, testInfo) => {
|
|
test.skip(!isDesktopProject(testInfo), "Desktop only");
|
|
|
|
await seedLimitedBackofficeSession(page);
|
|
const api = await mockLimitedBackofficeApi(page, sessionData, {
|
|
loginLinkMessage: "Missing employee management permission.",
|
|
loginLinkStatus: 403,
|
|
});
|
|
|
|
await page.goto("/backoffice/employees");
|
|
|
|
await page.getByTestId("limited-employee-login-link-501").click();
|
|
|
|
await expect(page.getByTestId("limited-employees-error")).toHaveText("Missing employee management permission.");
|
|
await expect(page.getByTestId("limited-login-link-modal")).toHaveCount(0);
|
|
expect(api.loginLinkCalls).toEqual(["501"]);
|
|
expect(api.forbiddenCalls).toEqual([]);
|
|
});
|
|
|
|
test("shows grouped human-readable role permission help without raw permission keys", async ({ page }, testInfo) => {
|
|
await seedLimitedBackofficeSession(page);
|
|
await mockLimitedBackofficeApi(page);
|
|
|
|
await page.goto("/backoffice/employees");
|
|
|
|
const helpButton = page.getByTestId("limited-employee-role-help");
|
|
await expect(helpButton).toBeVisible();
|
|
await expect(helpButton).toHaveAttribute("title", "View role permissions");
|
|
|
|
if (isDesktopProject(testInfo)) {
|
|
await helpButton.hover();
|
|
await expect(page.locator(".tooltip-content").filter({ hasText: "View role permissions" })).toBeVisible();
|
|
}
|
|
|
|
await helpButton.click();
|
|
|
|
const modal = page.getByTestId("limited-role-permissions-modal");
|
|
await expect(modal).toBeVisible();
|
|
await expect(modal).toContainText("Role permissions");
|
|
await expect(modal).toContainText("Every capability is limited to the departments selected for the employee.");
|
|
await expect(page.getByTestId("limited-role-permissions-role-viewer")).toContainText("Deactivated");
|
|
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Cashier");
|
|
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Orders");
|
|
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Products");
|
|
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Customers");
|
|
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Vehicles");
|
|
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Attachments");
|
|
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Scanners");
|
|
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Bookings");
|
|
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("View orders");
|
|
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Complete orders");
|
|
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("View product catalog");
|
|
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Search customers");
|
|
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Search vehicles");
|
|
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Add order attachments");
|
|
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("View plate scans");
|
|
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Create bookings");
|
|
await expect(page.getByTestId("limited-role-permissions-role-cashier")).toContainText("Create order lines");
|
|
await expect(page.getByTestId("limited-role-permissions-role-department_admin")).toContainText(
|
|
"View customer pricing"
|
|
);
|
|
await expect(page.getByTestId("limited-role-permissions-role-department_admin")).toContainText(
|
|
"Manage customer pricing"
|
|
);
|
|
await expect(page.getByTestId("limited-role-permissions-role-viewer")).toContainText("Selected");
|
|
|
|
for (const rawPermission of [
|
|
"list_orders",
|
|
"add_order",
|
|
"fetch_order",
|
|
"edit_order_items",
|
|
"delete_order_items",
|
|
"search_customers",
|
|
"list_order_attachments",
|
|
"list_products",
|
|
"add_bookings",
|
|
"department_timebookings_entries_get",
|
|
"limited_backoffice_employees_manage",
|
|
"limited_backoffice_customer_pricing_view",
|
|
"limited_backoffice_customer_pricing_manage",
|
|
"department_access_1",
|
|
"raw_permissions",
|
|
]) {
|
|
await expect(page.locator("body")).not.toContainText(rawPermission);
|
|
}
|
|
|
|
await page.getByTestId("limited-role-permissions-close-footer").click();
|
|
await expect(page.getByTestId("limited-role-permissions-modal")).toHaveCount(0);
|
|
|
|
await helpButton.click();
|
|
await expect(page.getByTestId("limited-role-permissions-modal")).toBeVisible();
|
|
await page.keyboard.press("Escape");
|
|
await expect(page.getByTestId("limited-role-permissions-modal")).toHaveCount(0);
|
|
});
|
|
|
|
test("validates employee fields and submits optional phone details", async ({ page }) => {
|
|
await seedLimitedBackofficeSession(page);
|
|
const api = await mockLimitedBackofficeApi(page);
|
|
|
|
await page.goto("/backoffice/employees");
|
|
|
|
await expect(page.getByTestId("limited-employees-title")).toBeVisible();
|
|
await expect(page.getByTestId("limited-employee-save")).toBeDisabled();
|
|
await expect(page.getByTestId("limited-employee-save")).toHaveClass(/is-fullwidth/);
|
|
await expect(page.getByTestId("limited-employee-departments").locator(".switch")).toHaveCount(2);
|
|
await expect(page.getByTestId("limited-employee-department-1")).toBeChecked();
|
|
await expect(page.getByTestId("limited-employee-department-2")).not.toBeChecked();
|
|
await expect(page.getByTestId("limited-employee-phone-country-code")).toContainText("+45");
|
|
|
|
await page.getByTestId("limited-employee-name").fill("No Phone Worker");
|
|
await page.getByTestId("limited-employee-email").fill("no-phone@example.com");
|
|
await page.getByTestId("limited-employee-password").fill("Secret123!");
|
|
await expect(page.getByTestId("limited-employee-save")).toBeEnabled();
|
|
await page.getByTestId("limited-employee-save").click();
|
|
|
|
await expect.poll(() => api.employeeCreateCalls.length).toBe(1);
|
|
expect(api.employeeCreateCalls[0]).toEqual({
|
|
display_name: "No Phone Worker",
|
|
email: "no-phone@example.com",
|
|
phone_country_code: null,
|
|
phone: null,
|
|
password: "Secret123!",
|
|
role_key: "viewer",
|
|
department_ids: [1],
|
|
});
|
|
await expect(page.getByTestId("limited-employee-row-903")).toContainText("No Phone Worker");
|
|
|
|
await page.getByTestId("limited-employee-name").fill("Phone Worker");
|
|
await page.getByTestId("limited-employee-email").fill("phone@example.com");
|
|
await page.getByTestId("limited-employee-phone-country-code").selectOption("358");
|
|
await page.getByTestId("limited-employee-phone").fill("87654321");
|
|
await page.getByTestId("limited-employee-password").fill("Secret123!");
|
|
await expect(page.getByTestId("limited-employee-save")).toBeEnabled();
|
|
await page.getByTestId("limited-employee-save").click();
|
|
|
|
await expect.poll(() => api.employeeCreateCalls.length).toBe(2);
|
|
expect(api.employeeCreateCalls[1]).toEqual({
|
|
display_name: "Phone Worker",
|
|
email: "phone@example.com",
|
|
phone_country_code: 358,
|
|
phone: 87654321,
|
|
password: "Secret123!",
|
|
role_key: "viewer",
|
|
department_ids: [1],
|
|
});
|
|
await expect(page.getByTestId("limited-employee-row-904")).toContainText("Phone Worker");
|
|
await expect(page.getByTestId("limited-employee-phone-904")).toHaveText("+358 87654321");
|
|
});
|
|
|
|
test("edits employee contact details without requiring a new password", async ({ page }) => {
|
|
await seedLimitedBackofficeSession(page);
|
|
const api = await mockLimitedBackofficeApi(page);
|
|
|
|
await page.goto("/backoffice/employees");
|
|
|
|
await expect(page.getByTestId("limited-employee-row-501")).toBeVisible();
|
|
await page.getByTestId("limited-employee-edit-501").click();
|
|
|
|
await expect(page.getByTestId("limited-employee-password")).toHaveValue("");
|
|
await expect(page.getByTestId("limited-employee-save")).toBeEnabled();
|
|
await page.getByTestId("limited-employee-name").fill("Casey Lead");
|
|
await page.getByTestId("limited-employee-email").fill("casey.lead@example.com");
|
|
await page.getByTestId("limited-employee-phone-country-code").selectOption("358");
|
|
await page.getByTestId("limited-employee-phone").fill("87654321");
|
|
await page.getByTestId("limited-employee-save").click();
|
|
|
|
await expect.poll(() => api.employeeUpdateCalls.length).toBe(1);
|
|
expect(api.employeeUpdateCalls[0]).toEqual({
|
|
display_name: "Casey Lead",
|
|
email: "casey.lead@example.com",
|
|
phone_country_code: 358,
|
|
phone: 87654321,
|
|
role_key: "cashier",
|
|
department_ids: [1],
|
|
});
|
|
expect(api.employeeUpdateCalls[0]).not.toHaveProperty("password");
|
|
await expect(page.getByTestId("limited-employee-row-501")).toContainText("Casey Lead");
|
|
await expect(page.getByTestId("limited-employee-phone-501")).toHaveText("+358 87654321");
|
|
});
|
|
|
|
test("keeps department context across employee access navigation", async ({ page }, testInfo) => {
|
|
test.skip(!isDesktopProject(testInfo), "Desktop only");
|
|
|
|
await seedLimitedBackofficeSession(page);
|
|
const api = await mockLimitedBackofficeApi(page);
|
|
|
|
await page.goto("/backoffice/employees");
|
|
|
|
await expect(page).toHaveURL(/\/backoffice\/departments\/1\/employees$/);
|
|
await expect(page.getByTestId("limited-backoffice-tab-prices")).toHaveAttribute(
|
|
"href",
|
|
"/backoffice/departments/1/prices"
|
|
);
|
|
await expect(page.getByTestId("limited-backoffice-tab-employees")).toHaveAttribute(
|
|
"href",
|
|
"/backoffice/departments/1/employees"
|
|
);
|
|
|
|
await page.getByTestId("limited-backoffice-department-select").selectOption("2");
|
|
|
|
await expect(page).toHaveURL(/\/backoffice\/departments\/2\/employees$/);
|
|
await expect(page.getByTestId("limited-employee-row-501")).toHaveCount(0);
|
|
await expect(page.getByTestId("limited-employee-row-502")).toContainText("Riley Remote");
|
|
await expect(page.getByTestId("limited-employee-department-1")).not.toBeChecked();
|
|
await expect(page.getByTestId("limited-employee-department-2")).toBeChecked();
|
|
await expect(page.getByTestId("limited-backoffice-tab-prices")).toHaveAttribute(
|
|
"href",
|
|
"/backoffice/departments/2/prices"
|
|
);
|
|
|
|
await page.getByTestId("limited-backoffice-tab-prices").click();
|
|
await expect(page).toHaveURL(/\/backoffice\/departments\/2\/prices$/);
|
|
await expect(page.getByTestId("limited-backoffice-tab-employees")).toHaveAttribute(
|
|
"href",
|
|
"/backoffice/departments/2/employees"
|
|
);
|
|
|
|
expect(api.forbiddenCalls).toEqual([]);
|
|
});
|
|
|
|
test("does not render data for a department outside the manager scope", async ({ page }, testInfo) => {
|
|
test.skip(!isDesktopProject(testInfo), "Desktop only");
|
|
|
|
await seedLimitedBackofficeSession(page);
|
|
const api = await mockLimitedBackofficeApi(page);
|
|
|
|
await page.goto("/backoffice/departments/3/prices");
|
|
|
|
await expect(page.getByTestId("limited-prices-forbidden")).toBeVisible();
|
|
await expect(page.locator("body")).not.toContainText("Secret Depot");
|
|
await expect(page.getByTestId("limited-prices-table")).toHaveCount(0);
|
|
|
|
expect(api.calls.some((call) => call.includes("/limited-backoffice/departments/3/prices"))).toBe(false);
|
|
expect(api.forbiddenCalls).toEqual([]);
|
|
});
|
|
|
|
test("does not render employee data for a department outside the manager scope", async ({ page }, testInfo) => {
|
|
test.skip(!isDesktopProject(testInfo), "Desktop only");
|
|
|
|
await seedLimitedBackofficeSession(page);
|
|
const api = await mockLimitedBackofficeApi(page);
|
|
|
|
await page.goto("/backoffice/departments/3/employees");
|
|
|
|
await expect(page.getByTestId("limited-employees-forbidden")).toBeVisible();
|
|
await expect(page.locator("body")).not.toContainText("Secret Depot");
|
|
await expect(page.getByTestId("limited-employees-table")).toHaveCount(0);
|
|
|
|
expect(api.calls.some((call) => call.includes("/limited-backoffice/employees"))).toBe(false);
|
|
expect(api.forbiddenCalls).toEqual([]);
|
|
});
|
|
});
|