- 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.
130 lines
3.5 KiB
TypeScript
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;
|
|
}
|