- Introduced `.prettierrc.json` to enforce consistent code formatting across the project. - Updated unit and e2e test files to address formatting issues, improve readability, and ensure alignment with the new Prettier configuration.
544 lines
17 KiB
JavaScript
544 lines
17 KiB
JavaScript
import { expect, test } from "@playwright/test";
|
|
import { mockApi, seedAuthenticatedState } from "./support/network.js";
|
|
|
|
function json(body, status = 200) {
|
|
return {
|
|
status,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(body),
|
|
};
|
|
}
|
|
|
|
function matchesApiPath(urlString, expectedPath) {
|
|
const url = new URL(urlString);
|
|
return url.pathname === expectedPath || url.pathname === `/api${expectedPath}`;
|
|
}
|
|
|
|
async function suppressVueDevtoolsOverlay(page) {
|
|
await page.addInitScript(() => {
|
|
const STYLE_ID = "__e2e-hide-vue-devtools";
|
|
|
|
const apply = () => {
|
|
const target = document.head || document.documentElement;
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
if (!document.getElementById(STYLE_ID)) {
|
|
const style = document.createElement("style");
|
|
style.id = STYLE_ID;
|
|
style.textContent =
|
|
"#__vue-devtools-container__, .vue-devtools__anchor-btn, .vue-devtools__panel-content { display: none !important; visibility: hidden !important; pointer-events: none !important; }";
|
|
target.appendChild(style);
|
|
}
|
|
|
|
const container = document.getElementById("__vue-devtools-container__");
|
|
if (container) {
|
|
container.style.display = "none";
|
|
container.style.pointerEvents = "none";
|
|
}
|
|
};
|
|
|
|
apply();
|
|
const observer = new MutationObserver(apply);
|
|
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
});
|
|
}
|
|
|
|
function createPeriodPayload() {
|
|
return {
|
|
types: {
|
|
all: [
|
|
{
|
|
id: 11,
|
|
customer_number: 4001,
|
|
customer_name: "Acme Fleet",
|
|
requires_action: true,
|
|
transactions: [{ id: 9001, date: "2026-03-10T10:00:00.000Z", amount: 120, booked: false, excluded: false }],
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
meta: {
|
|
fixed_pricing: {
|
|
price: 500,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: 12,
|
|
customer_number: 4002,
|
|
customer_name: "Nordic Transport",
|
|
requires_action: false,
|
|
transactions: [{ id: 9002, date: "2026-03-11T10:00:00.000Z", amount: 80, booked: true, excluded: false }],
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
meta: {},
|
|
},
|
|
],
|
|
invoice_per_order: [
|
|
{
|
|
id: 13,
|
|
customer_number: 7001,
|
|
customer_name: "Invoice Per Order Co",
|
|
requires_action: true,
|
|
transactions: [{ id: 9003, date: "2026-03-12T10:00:00.000Z", amount: 75, booked: false, excluded: false }],
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
meta: {},
|
|
},
|
|
],
|
|
fixed_pricing: [
|
|
{
|
|
id: 11,
|
|
customer_number: 4001,
|
|
customer_name: "Acme Fleet",
|
|
requires_action: true,
|
|
transactions: [],
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
meta: {
|
|
fixed_pricing: {
|
|
price: 500,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
tank_cleaning: [],
|
|
special_arrangements: [],
|
|
vehicle_subscriptions: [
|
|
{
|
|
id: 14,
|
|
customer_number: 8001,
|
|
customer_name: "Subscription Movers",
|
|
requires_action: true,
|
|
transactions: [{ id: 9004, date: "2026-03-13T10:00:00.000Z", amount: 60, booked: false, excluded: false }],
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
meta: {},
|
|
},
|
|
],
|
|
possible_duplicates: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
function createPeriodPayloadForChangedRange() {
|
|
return {
|
|
types: {
|
|
all: [
|
|
{
|
|
id: 21,
|
|
customer_number: 5001,
|
|
customer_name: "April Logistics",
|
|
requires_action: true,
|
|
transactions: [{ id: 9101, date: "2026-04-10T10:00:00.000Z", amount: 250, booked: false, excluded: false }],
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
meta: {},
|
|
},
|
|
],
|
|
invoice_per_order: [
|
|
{
|
|
id: 22,
|
|
customer_number: 5002,
|
|
customer_name: "April Per Order",
|
|
requires_action: true,
|
|
transactions: [{ id: 9102, date: "2026-04-11T11:00:00.000Z", amount: 150, booked: false, excluded: false }],
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
meta: {},
|
|
},
|
|
],
|
|
fixed_pricing: [],
|
|
tank_cleaning: [],
|
|
special_arrangements: [],
|
|
vehicle_subscriptions: [],
|
|
possible_duplicates: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
function createQueuedPeriodPayload() {
|
|
return {
|
|
types: {
|
|
all: [
|
|
{
|
|
id: 31,
|
|
customer_number: 6001,
|
|
customer_name: "Queued Fleet",
|
|
requires_action: false,
|
|
transactions: [
|
|
{
|
|
id: 9201,
|
|
date: "2026-04-14T10:00:00.000Z",
|
|
amount: 210,
|
|
booked: false,
|
|
excluded: false,
|
|
invoice_collection_id: 14578,
|
|
queue_status: "QUEUED",
|
|
queue_job_id: 88,
|
|
},
|
|
],
|
|
queue: {
|
|
has_active_job: true,
|
|
statuses: ["QUEUED"],
|
|
invoice_collection_ids: [14578],
|
|
is_action_blocked: true,
|
|
},
|
|
meta: {},
|
|
},
|
|
],
|
|
invoice_per_order: [],
|
|
fixed_pricing: [],
|
|
tank_cleaning: [],
|
|
special_arrangements: [],
|
|
vehicle_subscriptions: [],
|
|
possible_duplicates: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
async function setupPeriodEndpoints(page, requests) {
|
|
let initialRange = null;
|
|
|
|
await page.route("**/superuser/invoicing/period**", async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
const url = new URL(route.request().url());
|
|
if (!matchesApiPath(route.request().url(), "/superuser/invoicing/period")) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
const dateFrom = url.searchParams.get("dateFrom");
|
|
const dateTo = url.searchParams.get("dateTo");
|
|
requests.push({
|
|
dateFrom,
|
|
dateTo,
|
|
});
|
|
|
|
if (!initialRange) {
|
|
initialRange = { dateFrom, dateTo };
|
|
}
|
|
|
|
const didChangeRange = !initialRange || dateFrom !== initialRange.dateFrom || dateTo !== initialRange.dateTo;
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: didChangeRange ? createPeriodPayloadForChangedRange() : createPeriodPayload(),
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.route("**/superuser/invoicing/period/distribution/fixed-pricing**", async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
includes: {
|
|
collective_fixed_pricing_results: {
|
|
total_fixed_price: 500,
|
|
total_original_price: 400,
|
|
total_department_totals: {
|
|
1: 220,
|
|
2: 180,
|
|
},
|
|
total_department_totals_parsed: {
|
|
Copenhagen: 220,
|
|
Odense: 180,
|
|
},
|
|
total_department_totals_relative_parsed: {
|
|
Copenhagen: 260,
|
|
Odense: 240,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.route("**/superuser/invoicing/period/distribution/wash-subscriptions**", async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
includes: {
|
|
collective_subscription_results: {
|
|
total_subscription_price: 300,
|
|
total_fixed_price: 300,
|
|
total_original_price: 300,
|
|
total_department_totals: {
|
|
1: 200,
|
|
2: 100,
|
|
},
|
|
subscription_price_department_distribution_parsed: {
|
|
Copenhagen: 200,
|
|
Odense: 100,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.route("**/departments**", async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
const url = new URL(route.request().url());
|
|
if (!(url.pathname === "/departments" || url.pathname === "/api/departments")) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: [
|
|
{ id: 1, name: "Copenhagen" },
|
|
{ id: 2, name: "Odense" },
|
|
],
|
|
})
|
|
);
|
|
});
|
|
}
|
|
|
|
async function openPeriodView(page) {
|
|
const periodRequests = [];
|
|
const token = "superuser-period-e2e-token";
|
|
await suppressVueDevtoolsOverlay(page);
|
|
await seedAuthenticatedState(page, token);
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
loginToken: token,
|
|
invoiceDistribution: true,
|
|
});
|
|
await setupPeriodEndpoints(page, periodRequests);
|
|
await page.goto("/superuser/invoices?activeTab=period");
|
|
await expect(page).toHaveURL(/activeTab=period/);
|
|
await expect(page.getByTestId("invoicing-period-view")).toBeVisible();
|
|
return { periodRequests };
|
|
}
|
|
|
|
async function seedInvoicesPage(page, token = "superuser-period-e2e-token") {
|
|
await suppressVueDevtoolsOverlay(page);
|
|
await seedAuthenticatedState(page, token);
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
loginToken: token,
|
|
invoiceDistribution: true,
|
|
});
|
|
}
|
|
|
|
test.describe("Invoicing period tab", () => {
|
|
test("@smoke period view does not throw queue refresh errors on load", async ({ page }) => {
|
|
const pageErrors = [];
|
|
page.on("pageerror", (error) => {
|
|
pageErrors.push(error.message);
|
|
});
|
|
|
|
await openPeriodView(page);
|
|
await expect(page.getByTestId("invoicing-period-view-selector-all")).toBeVisible();
|
|
await page.waitForTimeout(250);
|
|
|
|
expect(pageErrors).not.toEqual(expect.arrayContaining([expect.stringContaining("reading 'value'")]));
|
|
});
|
|
|
|
test("@smoke period view loads selectors and displays all-customer list", async ({ page }) => {
|
|
const { periodRequests } = await openPeriodView(page);
|
|
|
|
await expect(page.getByTestId("invoicing-period-view-selector-all")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-view-selector-invoice_per_order")).toBeVisible();
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
|
await expect(page.getByTestId("invoicing-period-view-all")).toHaveAttribute("data-current-view", "all");
|
|
await expect(page.getByTestId("invoicing-period-customer-4001")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-customer-4002")).toBeVisible();
|
|
expect(periodRequests.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("@smoke period view selector switch updates visible customer set", async ({ page }) => {
|
|
await openPeriodView(page);
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-invoice_per_order").click();
|
|
await expect(page.getByTestId("invoicing-period-view-all")).toHaveAttribute(
|
|
"data-current-view",
|
|
"invoice_per_order"
|
|
);
|
|
await expect(page.getByTestId("invoicing-period-customer-7001")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-customer-4001")).toHaveCount(0);
|
|
await expect(page).toHaveURL(/activeTab=period/);
|
|
});
|
|
|
|
test("@smoke period view shows invoice action only for customers requiring action", async ({ page }) => {
|
|
await openPeriodView(page);
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
|
await expect(page.getByTestId("invoicing-period-customer-4001")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-customer-invoice-4001")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-customer-4002")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-customer-invoice-4002")).toHaveCount(0);
|
|
await expect(page).toHaveURL(/activeTab=period/);
|
|
});
|
|
|
|
test("@smoke period view date changes trigger a refreshed period query", async ({ page }) => {
|
|
const { periodRequests } = await openPeriodView(page);
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
|
await expect(page.getByTestId("invoicing-period-customer-4001")).toBeVisible();
|
|
|
|
const dateInputs = page.locator("[data-testid='invoicing-period-view'] input[type='date']:visible");
|
|
await expect(dateInputs.first()).toBeVisible();
|
|
|
|
await dateInputs.nth(0).fill("2026-04-02");
|
|
await dateInputs.nth(0).dispatchEvent("change");
|
|
await dateInputs.nth(1).fill("2026-04-30");
|
|
await dateInputs.nth(1).dispatchEvent("change");
|
|
|
|
const firstRange = periodRequests[0];
|
|
await expect
|
|
.poll(() =>
|
|
periodRequests.some(
|
|
(request) => request.dateFrom !== firstRange?.dateFrom || request.dateTo !== firstRange?.dateTo
|
|
)
|
|
)
|
|
.toBeTruthy();
|
|
await expect(page.getByTestId("invoicing-period-customer-5001")).toBeVisible();
|
|
await expect(page).toHaveURL(/activeTab=period/);
|
|
});
|
|
|
|
test("@smoke period view reload button triggers a fresh period query", async ({ page }, testInfo) => {
|
|
test.skip(/mobile/i.test(testInfo.project.name), "Reload button is not rendered in mobile date selector layout.");
|
|
|
|
const pageErrors = [];
|
|
page.on("pageerror", (error) => {
|
|
pageErrors.push(error.message);
|
|
});
|
|
|
|
const { periodRequests } = await openPeriodView(page);
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
|
const initialRequestCount = periodRequests.length;
|
|
await page.getByTestId("invoicing-period-reload-button").click();
|
|
await expect.poll(() => periodRequests.length).toBeGreaterThan(initialRequestCount);
|
|
await page.waitForTimeout(250);
|
|
await expect(page.getByTestId("invoicing-period-view-all")).toHaveAttribute("data-current-view", "all");
|
|
await expect(page).toHaveURL(/activeTab=period/);
|
|
expect(pageErrors).not.toEqual(expect.arrayContaining([expect.stringContaining("reading 'types'")]));
|
|
});
|
|
|
|
test("@smoke period view shows a queued CTA when backend queue metadata blocks invoicing", async ({ page }) => {
|
|
const periodRequests = [];
|
|
await seedInvoicesPage(page);
|
|
await setupPeriodEndpoints(page, periodRequests);
|
|
|
|
await page.route("**/superuser/invoicing/period**", async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
const url = new URL(route.request().url());
|
|
periodRequests.push({
|
|
dateFrom: url.searchParams.get("dateFrom"),
|
|
dateTo: url.searchParams.get("dateTo"),
|
|
});
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: createQueuedPeriodPayload(),
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.goto("/superuser/invoices?activeTab=period");
|
|
await expect(page).toHaveURL(/activeTab=period/);
|
|
await expect(page.getByTestId("invoicing-period-view")).toBeVisible();
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
|
await expect(page.getByTestId("invoicing-period-customer-6001")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-customer-queue-6001")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-customer-queue-6001")).toBeDisabled();
|
|
await expect(page.getByTestId("invoicing-period-customer-invoice-6001")).toHaveCount(0);
|
|
expect(periodRequests.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("@smoke period view refreshes queue state when the Period route becomes active again", async ({ page }) => {
|
|
const periodRequests = [];
|
|
let requestCount = 0;
|
|
let returnQueuedPayload = false;
|
|
|
|
await seedInvoicesPage(page);
|
|
await setupPeriodEndpoints(page, periodRequests);
|
|
|
|
await page.route("**/superuser/invoicing/period**", async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
requestCount += 1;
|
|
const url = new URL(route.request().url());
|
|
periodRequests.push({
|
|
dateFrom: url.searchParams.get("dateFrom"),
|
|
dateTo: url.searchParams.get("dateTo"),
|
|
});
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: returnQueuedPayload ? createQueuedPeriodPayload() : createPeriodPayload(),
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.goto("/superuser/invoices?activeTab=overview");
|
|
await expect(page).toHaveURL(/activeTab=overview/);
|
|
returnQueuedPayload = true;
|
|
|
|
await page.goto("/superuser/invoices?activeTab=period");
|
|
await expect(page).toHaveURL(/activeTab=period/);
|
|
await expect.poll(() => requestCount).toBeGreaterThan(0);
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
|
await expect(page.getByTestId("invoicing-period-customer-queue-6001")).toBeVisible();
|
|
});
|
|
});
|