Files
pleno-vue/tests/e2e/release/helpers.ts
T
Jeppe Bundgaard f040614f46 Refactor module paths, layout responsiveness, and testing configurations:
- Replaced repetitive department module URL logic with `buildDepartmentModulePath` utility in `DepartmentModulesDisplay.vue`.
- Enhanced layouts and responsive behavior across components, including `PosDepartmentStep1MobileTransactionHistory.vue` and `DepartmentDailyReport.vue`.
- Removed unused `SuperuserInvoicingLocalStore.vue` and refactored to `SuperuserInvoicingLocalStore.ts`.
- Updated media query handling and card layout adjustments in `DepartmentDailyReport.vue`.
- Added new Playwright configurations (`playwright.prod.config.ts` and `playwright.live.config.ts`) for e2e release workflows.
- Extended e2e and unit test coverage for mobile POS and department modules.
2026-04-09 16:32:25 +02:00

130 lines
3.5 KiB
TypeScript

import { expect, type Locator, type Page, type Request, type Response } from "@playwright/test";
const CRITICAL_RESOURCE_TYPES = new Set(["document", "stylesheet", "script", "image", "font"]);
const ABORTED_REQUEST_PATTERNS = ["ERR_ABORTED", "NS_BINDING_ABORTED"];
const BENIGN_PAGE_ERRORS = ["ResizeObserver loop limit exceeded"];
function isSameOrigin(url: string, baseOrigin: string) {
try {
return new URL(url).origin === baseOrigin;
} catch {
return false;
}
}
function isCriticalResource(url: string, resourceType: string) {
if (CRITICAL_RESOURCE_TYPES.has(resourceType)) {
return true;
}
try {
const pathname = new URL(url).pathname;
return /manifest(\.webmanifest|\.json)$/i.test(pathname);
} catch {
return false;
}
}
export function attachPageHealthGuards(page: Page, baseURL: string) {
const baseOrigin = new URL(baseURL).origin;
const pageErrors: string[] = [];
const failedResources: string[] = [];
const onPageError = (error: Error) => {
if (BENIGN_PAGE_ERRORS.some((pattern) => error.message.includes(pattern))) {
return;
}
pageErrors.push(error.message);
};
const onRequestFailed = (request: Request) => {
const errorText = request.failure()?.errorText || "Request failed";
if (ABORTED_REQUEST_PATTERNS.some((pattern) => errorText.includes(pattern))) {
return;
}
if (!isSameOrigin(request.url(), baseOrigin)) {
return;
}
const resourceType = request.resourceType();
if (!isCriticalResource(request.url(), resourceType)) {
return;
}
failedResources.push(`${resourceType} ${request.url()} (${errorText})`);
};
const onResponse = (response: Response) => {
if (response.status() < 400) {
return;
}
const request = response.request();
if (!isSameOrigin(response.url(), baseOrigin)) {
return;
}
const resourceType = request.resourceType();
if (!isCriticalResource(response.url(), resourceType)) {
return;
}
failedResources.push(`${response.status()} ${resourceType} ${response.url()}`);
};
page.on("pageerror", onPageError);
page.on("requestfailed", onRequestFailed);
page.on("response", onResponse);
return {
async expectHealthy() {
expect(pageErrors, "Unexpected uncaught browser errors were recorded.").toEqual([]);
expect(failedResources, "Critical same-origin resources failed to load.").toEqual([]);
},
dispose() {
page.off("pageerror", onPageError);
page.off("requestfailed", onRequestFailed);
page.off("response", onResponse);
},
};
}
export async function expectBodyHasContent(page: Page, minimumLength = 20) {
await expect
.poll(async () => {
const text = await page.locator("body").innerText().catch(() => "");
return text.replace(/\s+/g, " ").trim().length;
}, { timeout: 15_000 })
.toBeGreaterThan(minimumLength);
}
export async function settlePage(page: Page) {
await page.waitForLoadState("domcontentloaded");
await page.waitForLoadState("networkidle", { timeout: 10_000 }).catch(() => {});
}
export async function expectOneVisible(locators: Locator[], timeout = 15_000) {
await expect
.poll(async () => {
for (const locator of locators) {
if (await locator.isVisible().catch(() => false)) {
return true;
}
}
return false;
}, { timeout })
.toBe(true);
}
export function requiredEnv(name: string) {
const value = process.env[name];
if (!value) {
throw new Error(`${name} is required for the live smoke gate.`);
}
return value;
}