- 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.
611 lines
21 KiB
JavaScript
611 lines
21 KiB
JavaScript
import { expect, test } from "@playwright/test";
|
|
import { mockApi, seedAuthenticatedState } from "./support/network.js";
|
|
|
|
const QUEUE_LIST_PATH = "/collected-invoices/economic/queue";
|
|
const QUEUE_STATUS_PATH = "/collected-invoices/economic/queue/status";
|
|
const QUEUE_RUN_PATH = "/collected-invoices/economic/queue/run";
|
|
const QUEUE_RETRY_PATH = "/collected-invoices/economic/queue/retry";
|
|
|
|
function json(body, status = 200) {
|
|
return {
|
|
status,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(body),
|
|
};
|
|
}
|
|
|
|
function isQueuePath(requestUrl, path) {
|
|
const url = new URL(requestUrl);
|
|
return url.pathname === path || url.pathname === `/api${path}`;
|
|
}
|
|
|
|
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 });
|
|
});
|
|
}
|
|
|
|
async function bootstrapAuthenticatedSuperuser(page) {
|
|
const token = "superuser-queue-history-e2e-token";
|
|
await suppressVueDevtoolsOverlay(page);
|
|
await seedAuthenticatedState(page, token);
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
loginToken: token,
|
|
});
|
|
return token;
|
|
}
|
|
|
|
async function openQueueHistory(page, token) {
|
|
await page.goto("/");
|
|
await page.evaluate((value) => {
|
|
window.localStorage.setItem("token", value);
|
|
}, token);
|
|
await page.goto("/superuser/invoices?activeTab=queue");
|
|
await expect(page).toHaveURL(/activeTab=queue/);
|
|
await expect(page.getByTestId("economic-queue-history-page")).toBeVisible();
|
|
}
|
|
|
|
test.describe("Invoice transfer queue history reliability", () => {
|
|
test("@smoke polling progression reaches terminal state and stops", async ({ page }) => {
|
|
let queueListCalls = 0;
|
|
const token = await bootstrapAuthenticatedSuperuser(page);
|
|
|
|
await page.route("**/collected-invoices/economic/queue**", async (route) => {
|
|
if (route.request().method() !== "GET" || !isQueuePath(route.request().url(), QUEUE_LIST_PATH)) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
queueListCalls += 1;
|
|
const isTerminal = queueListCalls > 1;
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
items: [
|
|
{
|
|
id: 9301,
|
|
status: isTerminal ? "COMPLETED" : "PROCESSING",
|
|
progress_percent: isTerminal ? 100 : 45,
|
|
progress_message: isTerminal ? "Completed" : "Exporting line items",
|
|
collected_invoice_id: 7001,
|
|
attempts: 1,
|
|
max_attempts: 3,
|
|
created_at: "2026-04-08T10:00:00Z",
|
|
updated_at: "2026-04-08T10:00:00Z",
|
|
},
|
|
],
|
|
count: 1,
|
|
total: 1,
|
|
limit: 50,
|
|
offset: 0,
|
|
has_more: false,
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await openQueueHistory(page, token);
|
|
|
|
await expect(page.getByTestId("economic-queue-history-status-9301")).toContainText("PROCESSING");
|
|
await expect
|
|
.poll(async () => page.getByTestId("economic-queue-history-status-9301").innerText(), {
|
|
timeout: 12_000,
|
|
})
|
|
.toContain("COMPLETED");
|
|
|
|
const callsAtTerminal = queueListCalls;
|
|
await page.waitForTimeout(5_000);
|
|
expect(queueListCalls).toBe(callsAtTerminal);
|
|
});
|
|
|
|
test("@smoke status/limit/offset filters follow server metadata pagination", async ({ page }) => {
|
|
const queueRequests = [];
|
|
const token = await bootstrapAuthenticatedSuperuser(page);
|
|
|
|
await page.route("**/collected-invoices/economic/queue**", async (route) => {
|
|
if (route.request().method() !== "GET" || !isQueuePath(route.request().url(), QUEUE_LIST_PATH)) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
const url = new URL(route.request().url());
|
|
const status = url.searchParams.get("status");
|
|
const limit = Number(url.searchParams.get("limit") || "50");
|
|
const offset = Number(url.searchParams.get("offset") || "0");
|
|
|
|
queueRequests.push({ status, limit, offset });
|
|
|
|
const hasMore = offset < 10;
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
items: [
|
|
{
|
|
id: hasMore ? 9501 : 9502,
|
|
status: "FAILED",
|
|
progress_percent: hasMore ? 20 : 100,
|
|
progress_message: hasMore ? "Failed" : "Failed and exhausted",
|
|
error_message: hasMore ? "Transient API timeout" : "Permanent validation failure",
|
|
collected_invoice_id: hasMore ? 7301 : 7302,
|
|
attempts: hasMore ? 1 : 3,
|
|
max_attempts: 3,
|
|
created_at: "2026-04-08T11:00:00Z",
|
|
updated_at: "2026-04-08T11:00:30Z",
|
|
},
|
|
],
|
|
count: 1,
|
|
total: 11,
|
|
limit,
|
|
offset,
|
|
has_more: hasMore,
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await openQueueHistory(page, token);
|
|
|
|
await page.getByTestId("economic-queue-history-status-filter").selectOption("FAILED");
|
|
await page.getByTestId("economic-queue-history-limit-filter").selectOption("10");
|
|
|
|
await expect
|
|
.poll(
|
|
() =>
|
|
queueRequests.some((request) => request.status === "FAILED" && request.limit === 10 && request.offset === 0),
|
|
{ timeout: 8_000 }
|
|
)
|
|
.toBeTruthy();
|
|
await expect(page.getByTestId("economic-queue-history-summary")).toContainText("More results available.");
|
|
|
|
await page.getByTestId("economic-queue-history-next-page").click();
|
|
|
|
await expect
|
|
.poll(
|
|
() =>
|
|
queueRequests.some((request) => request.status === "FAILED" && request.limit === 10 && request.offset === 10),
|
|
{ timeout: 8_000 }
|
|
)
|
|
.toBeTruthy();
|
|
await expect(page.getByTestId("economic-queue-history-summary")).not.toContainText("More results available.");
|
|
await expect(page.getByTestId("economic-queue-history-next-page")).toBeDisabled();
|
|
});
|
|
|
|
test("@smoke run queue now falls back when the backend route is ahead of the queue class deployment", async ({
|
|
page,
|
|
}) => {
|
|
let statusCalls = 0;
|
|
let queueListCalls = 0;
|
|
const token = await bootstrapAuthenticatedSuperuser(page);
|
|
|
|
await page.route("**/collected-invoices/economic/queue/run**", async (route) => {
|
|
if (route.request().method() !== "POST" || !isQueuePath(route.request().url(), QUEUE_RUN_PATH)) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json(
|
|
{
|
|
message:
|
|
"Internal server error: Call to undefined method classes\\economic_transfer_queue::processPendingByTransferType()",
|
|
},
|
|
500
|
|
)
|
|
);
|
|
});
|
|
|
|
await page.route("**/collected-invoices/economic/queue/status**", async (route) => {
|
|
if (route.request().method() !== "GET" || !isQueuePath(route.request().url(), QUEUE_STATUS_PATH)) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
statusCalls += 1;
|
|
const url = new URL(route.request().url());
|
|
expect(url.searchParams.get("job_id")).toBe("9351");
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
id: 9351,
|
|
status: "PROCESSING",
|
|
progress_percent: 45,
|
|
progress_message: "Legacy worker tick",
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.route("**/collected-invoices/economic/queue**", async (route) => {
|
|
if (route.request().method() !== "GET" || !isQueuePath(route.request().url(), QUEUE_LIST_PATH)) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
queueListCalls += 1;
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
items: [
|
|
{
|
|
id: 9351,
|
|
status: queueListCalls === 1 ? "QUEUED" : "PROCESSING",
|
|
progress_percent: queueListCalls === 1 ? 0 : 45,
|
|
progress_message: queueListCalls === 1 ? "Queued" : "Legacy worker tick",
|
|
collected_invoice_id: 7351,
|
|
attempts: 0,
|
|
max_attempts: 3,
|
|
created_at: "2026-04-08T11:30:00Z",
|
|
updated_at: "2026-04-08T11:30:30Z",
|
|
},
|
|
],
|
|
count: 1,
|
|
total: 1,
|
|
limit: 50,
|
|
offset: 0,
|
|
has_more: false,
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await openQueueHistory(page, token);
|
|
|
|
await page.getByTestId("economic-queue-history-run-now").click();
|
|
|
|
await expect.poll(() => statusCalls, { timeout: 8_000 }).toBe(1);
|
|
await expect(page.getByTestId("economic-queue-history-run-summary")).toContainText("Legacy fallback active");
|
|
await expect(page.getByTestId("economic-queue-history-summary")).toContainText(
|
|
"Legacy fallback active while the backend queue runner is unavailable."
|
|
);
|
|
await expect(page.getByTestId("economic-queue-history-error")).toHaveCount(0);
|
|
await expect(page.getByTestId("economic-queue-history-status-9351")).toContainText("PROCESSING");
|
|
});
|
|
|
|
test("@smoke failed-job retry succeeds and non-failed retry stays unavailable", async ({ page }) => {
|
|
let retryCalls = 0;
|
|
let retryTriggered = false;
|
|
const token = await bootstrapAuthenticatedSuperuser(page);
|
|
|
|
await page.route("**/collected-invoices/economic/queue/retry**", async (route) => {
|
|
if (route.request().method() !== "POST" || !isQueuePath(route.request().url(), QUEUE_RETRY_PATH)) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
retryCalls += 1;
|
|
expect(route.request().postDataJSON()).toEqual({ job_id: 9402 });
|
|
retryTriggered = true;
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
job: {
|
|
id: 9402,
|
|
status: "QUEUED",
|
|
progress_percent: 0,
|
|
progress_message: "Retry queued",
|
|
},
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.route("**/collected-invoices/economic/queue**", async (route) => {
|
|
if (route.request().method() !== "GET" || !isQueuePath(route.request().url(), QUEUE_LIST_PATH)) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
items: [
|
|
{
|
|
id: 9401,
|
|
status: "COMPLETED",
|
|
progress_percent: 100,
|
|
progress_message: "Completed",
|
|
collected_invoice_id: 7401,
|
|
attempts: 1,
|
|
max_attempts: 3,
|
|
created_at: "2026-04-08T12:00:00Z",
|
|
updated_at: "2026-04-08T12:01:00Z",
|
|
},
|
|
{
|
|
id: 9402,
|
|
status: retryTriggered ? "QUEUED" : "FAILED",
|
|
progress_percent: retryTriggered ? 0 : 40,
|
|
progress_message: retryTriggered ? "Retry queued" : "Failed",
|
|
error_message: retryTriggered ? "" : "Temporary upstream issue",
|
|
collected_invoice_id: 7402,
|
|
attempts: retryTriggered ? 1 : 1,
|
|
max_attempts: 3,
|
|
created_at: "2026-04-08T12:00:00Z",
|
|
updated_at: "2026-04-08T12:01:00Z",
|
|
},
|
|
{
|
|
id: 9403,
|
|
status: "FAILED",
|
|
progress_percent: 100,
|
|
progress_message: "Failed",
|
|
error_message: "Max attempts reached",
|
|
collected_invoice_id: 7403,
|
|
attempts: 3,
|
|
max_attempts: 3,
|
|
created_at: "2026-04-08T12:00:00Z",
|
|
updated_at: "2026-04-08T12:01:00Z",
|
|
},
|
|
],
|
|
count: 3,
|
|
total: 3,
|
|
limit: 50,
|
|
offset: 0,
|
|
has_more: false,
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await openQueueHistory(page, token);
|
|
|
|
await expect(page.getByTestId("economic-queue-history-retry-9401")).toHaveCount(0);
|
|
await expect(page.getByTestId("economic-queue-history-retry-9403")).toBeDisabled();
|
|
|
|
await page.getByTestId("economic-queue-history-retry-9402").click();
|
|
await expect.poll(() => retryCalls, { timeout: 8_000 }).toBe(1);
|
|
await expect(page.getByTestId("economic-queue-history-status-9402")).toContainText("QUEUED");
|
|
await expect(page.getByTestId("economic-queue-history-retry-9402")).toHaveCount(0);
|
|
});
|
|
|
|
test("@smoke polling transport error is surfaced and recovery succeeds", async ({ page }) => {
|
|
let queueListCalls = 0;
|
|
const token = await bootstrapAuthenticatedSuperuser(page);
|
|
|
|
await page.route("**/collected-invoices/economic/queue**", async (route) => {
|
|
if (route.request().method() !== "GET" || !isQueuePath(route.request().url(), QUEUE_LIST_PATH)) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
queueListCalls += 1;
|
|
if (queueListCalls >= 2 && queueListCalls <= 4) {
|
|
await route.fulfill(
|
|
json(
|
|
{
|
|
data: {
|
|
message: "gateway timeout",
|
|
},
|
|
},
|
|
503
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
const isTerminal = queueListCalls >= 6;
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
items: [
|
|
{
|
|
id: 9601,
|
|
status: isTerminal ? "COMPLETED" : "PROCESSING",
|
|
progress_percent: isTerminal ? 100 : 60,
|
|
progress_message: isTerminal ? "Completed" : "Retrying worker connection",
|
|
collected_invoice_id: 7601,
|
|
attempts: 1,
|
|
max_attempts: 3,
|
|
created_at: "2026-04-08T13:00:00Z",
|
|
updated_at: "2026-04-08T13:00:30Z",
|
|
},
|
|
],
|
|
count: 1,
|
|
total: 1,
|
|
limit: 50,
|
|
offset: 0,
|
|
has_more: false,
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await openQueueHistory(page, token);
|
|
|
|
await expect(page.getByTestId("economic-queue-history-status-9601")).toContainText("PROCESSING");
|
|
await expect(page.getByTestId("economic-queue-history-poll-error")).toContainText("gateway timeout", {
|
|
timeout: 12_000,
|
|
});
|
|
|
|
await page.getByTestId("economic-queue-history-refresh").click();
|
|
await expect(page.getByTestId("economic-queue-history-poll-error")).toHaveCount(0);
|
|
|
|
await expect
|
|
.poll(async () => page.getByTestId("economic-queue-history-status-9601").innerText(), {
|
|
timeout: 12_000,
|
|
})
|
|
.toContain("COMPLETED");
|
|
await expect(page.getByTestId("economic-queue-history-error")).toHaveCount(0);
|
|
});
|
|
|
|
test("@smoke slow responses do not create overlapping polling requests", async ({ page }) => {
|
|
let queueListCalls = 0;
|
|
let inFlight = 0;
|
|
let maxInFlight = 0;
|
|
const token = await bootstrapAuthenticatedSuperuser(page);
|
|
|
|
await page.route("**/collected-invoices/economic/queue**", async (route) => {
|
|
if (route.request().method() !== "GET" || !isQueuePath(route.request().url(), QUEUE_LIST_PATH)) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
inFlight += 1;
|
|
maxInFlight = Math.max(maxInFlight, inFlight);
|
|
queueListCalls += 1;
|
|
const callNumber = queueListCalls;
|
|
|
|
try {
|
|
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
const isTerminal = callNumber >= 3;
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
items: [
|
|
{
|
|
id: 9701,
|
|
status: isTerminal ? "COMPLETED" : "PROCESSING",
|
|
progress_percent: isTerminal ? 100 : 55,
|
|
progress_message: isTerminal ? "Completed" : "Working",
|
|
collected_invoice_id: 7701,
|
|
attempts: 1,
|
|
max_attempts: 3,
|
|
created_at: "2026-04-08T14:00:00Z",
|
|
updated_at: "2026-04-08T14:00:30Z",
|
|
},
|
|
],
|
|
count: 1,
|
|
total: 1,
|
|
limit: 50,
|
|
offset: 0,
|
|
has_more: false,
|
|
},
|
|
})
|
|
);
|
|
} finally {
|
|
inFlight -= 1;
|
|
}
|
|
});
|
|
|
|
await openQueueHistory(page, token);
|
|
|
|
await expect.poll(() => queueListCalls, { timeout: 20_000 }).toBeGreaterThanOrEqual(3);
|
|
expect(maxInFlight).toBe(1);
|
|
await expect(page.getByTestId("economic-queue-history-status-9701")).toContainText("COMPLETED");
|
|
});
|
|
|
|
test("@smoke mobile layout parity keeps queue controls and actions accessible", async ({ page }, testInfo) => {
|
|
test.skip(!testInfo.project.name.includes("mobile"), "Mobile-specific layout assertions");
|
|
|
|
const token = await bootstrapAuthenticatedSuperuser(page);
|
|
|
|
await page.route("**/collected-invoices/economic/queue**", async (route) => {
|
|
if (route.request().method() !== "GET" || !isQueuePath(route.request().url(), QUEUE_LIST_PATH)) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
items: [
|
|
{
|
|
id: 9801,
|
|
status: "FAILED",
|
|
progress_percent: 100,
|
|
progress_message: "Failed",
|
|
error_message: "Retry available",
|
|
collected_invoice_id: 7801,
|
|
attempts: 1,
|
|
max_attempts: 3,
|
|
created_at: "2026-04-08T15:00:00Z",
|
|
updated_at: "2026-04-08T15:00:30Z",
|
|
details_summary: {
|
|
message: "Queue failure details",
|
|
target: {
|
|
collected_invoice_id: 7801,
|
|
send_as_is: false,
|
|
requested_by: 7,
|
|
},
|
|
customer: {
|
|
customer_number: 43425425,
|
|
name: "Carrier A/S",
|
|
},
|
|
outcome: {
|
|
status: "FAILED",
|
|
error_message: "Retry available",
|
|
economic_invoice_draft_id: null,
|
|
economic_invoice_booked_id: null,
|
|
external_id: null,
|
|
total_net_amount: null,
|
|
order_count: 0,
|
|
},
|
|
transfer: {
|
|
transfer_type: "COLLECTED_INVOICE_EXPORT",
|
|
status: "FAILED",
|
|
progress_percent: 100,
|
|
progress_message: "Failed",
|
|
attempts: 1,
|
|
max_attempts: 3,
|
|
},
|
|
technical: {
|
|
job_id: 9801,
|
|
created_by: 7,
|
|
created_at: "2026-04-08T15:00:00Z",
|
|
updated_at: "2026-04-08T15:00:30Z",
|
|
raw_available: {
|
|
payload: true,
|
|
result: true,
|
|
},
|
|
raw: {
|
|
payload: {
|
|
collected_invoice_id: 7801,
|
|
},
|
|
result: null,
|
|
},
|
|
},
|
|
raw_available: {
|
|
payload: true,
|
|
result: true,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
count: 1,
|
|
total: 1,
|
|
limit: 50,
|
|
offset: 0,
|
|
has_more: false,
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await openQueueHistory(page, token);
|
|
|
|
await expect(page.getByTestId("economic-queue-history-run-now")).toBeVisible();
|
|
await expect(page.getByTestId("economic-queue-history-refresh")).toBeVisible();
|
|
await expect(page.getByTestId("economic-queue-history-prev-page")).toBeVisible();
|
|
await expect(page.getByTestId("economic-queue-history-next-page")).toBeVisible();
|
|
await expect(page.getByTestId("economic-queue-history-retry-9801")).toBeVisible();
|
|
|
|
await page.getByTestId("economic-queue-history-details-9801").click();
|
|
await expect(page.getByTestId("economic-queue-history-details-modal")).toBeVisible();
|
|
await expect(page.getByTestId("economic-queue-history-details-section-overfoersel")).toContainText("FAILED");
|
|
await page.getByTestId("economic-queue-history-details-close").click();
|
|
await expect(page.getByTestId("economic-queue-history-details-modal")).toHaveCount(0);
|
|
});
|
|
});
|