Files
pleno-vue/tests/e2e/invoicing-period.smoke.spec.js
T
Jeppe Bundgaard 2b6129db29 Add economic queue functionality with Vue component, unit tests, and e2e tests:
- Introduced `CollectedOrderInvoicesQueueHistory.vue` for displaying and managing job queue history.
- Added queue management features: polling with active status detection, error handling, retry logic, and pagination controls.
- Implemented e2e tests for economic queue workflows, including export polling, retries, validation errors, and component unmount behavior.
- Added unit tests for queue logic, payload normalization, and retry scenarios.
- Created `EconomicQueuePlaywrightHarness.vue` to support e2e testing with mock workflows.
2026-04-08 11:19:16 +02:00

342 lines
11 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),
};
}
async function suppressVueDevtoolsOverlay(page) {
await page.addInitScript(() => {
const STYLE_ID = "__e2e-hide-vue-devtools";
const apply = () => {
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; }";
(document.head || document.documentElement).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 },
],
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 },
],
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 },
],
meta: {},
},
],
fixed_pricing: [
{
id: 11,
customer_number: 4001,
customer_name: "Acme Fleet",
requires_action: true,
transactions: [],
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 },
],
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 },
],
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 },
],
meta: {},
},
],
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());
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,
});
await setupPeriodEndpoints(page, periodRequests);
await page.goto("/");
await page.evaluate((value) => {
window.localStorage.setItem("token", value);
}, token);
await page.goto("/superuser/invoices?activeTab=period");
await expect(page).toHaveURL(/activeTab=period/);
await expect(page.getByTestId("invoicing-period-view")).toBeVisible();
return { periodRequests };
}
test.describe("Invoicing period tab", () => {
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 { 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 expect(page.getByTestId("invoicing-period-view-all")).toHaveAttribute("data-current-view", "all");
await expect(page).toHaveURL(/activeTab=period/);
});
});