Add Playwright PR runner and Vitest unit test fast runner scripts; integrate with test commands and update specs/tests for PR-based selection.
This commit is contained in:
+10
-11
@@ -61,17 +61,18 @@ jobs:
|
||||
env:
|
||||
VITEST_BATCH_SIZE: 5
|
||||
|
||||
e2e-smoke:
|
||||
e2e-pr:
|
||||
if: github.event_name != 'schedule'
|
||||
needs: build-and-unit
|
||||
runs-on: [self-hosted, Linux, X64, default]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
project: [chromium-desktop, chromium-mobile]
|
||||
env:
|
||||
PLAYWRIGHT_PR_BASE: ${{ github.event.pull_request.base.sha || github.event.before }}
|
||||
PLAYWRIGHT_PR_HEAD: ${{ github.sha }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
@@ -85,17 +86,15 @@ jobs:
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Run Playwright smoke tests (${{ matrix.project }})
|
||||
run: npx playwright test --grep @smoke --project="${{ matrix.project }}"
|
||||
- name: Run Playwright PR tests
|
||||
run: npm run test:e2e:pr -- --base="$PLAYWRIGHT_PR_BASE" --head="$PLAYWRIGHT_PR_HEAD"
|
||||
|
||||
- name: Upload Playwright report
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report-smoke-${{ matrix.project }}
|
||||
path: |
|
||||
output/playwright/report
|
||||
output/playwright/test-results
|
||||
name: playwright-report-pr
|
||||
path: output/playwright
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
|
||||
|
||||
+8
-3
@@ -16,9 +16,14 @@
|
||||
"preview:prod": "npm run build && npm run preview -- --host 127.0.0.1 --port 4173",
|
||||
"text:fix-encoding": "node scripts/text-encoding.mjs fix",
|
||||
"text:check-encoding": "node scripts/text-encoding.mjs check",
|
||||
"test:unit": "npm run text:check-encoding && node scripts/run-vitest-unit-batches.mjs",
|
||||
"test:unit": "npm run text:check-encoding && npm run test:unit:fast && npm run test:unit:serial",
|
||||
"test:unit:fast": "node scripts/run-vitest-unit-fast.mjs",
|
||||
"test:unit:serial": "node scripts/run-vitest-unit-batches.mjs --from-list tests/unit/serial-tests.txt",
|
||||
"test:unit:single": "vitest run",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e": "npm run test:e2e:matrix",
|
||||
"test:e2e:matrix": "playwright test",
|
||||
"test:e2e:pr": "node scripts/run-playwright-pr.mjs",
|
||||
"test:e2e:changed": "node scripts/run-playwright-pr.mjs --changed-only",
|
||||
"test:e2e:i18n:views": "playwright test tests/e2e/i18n.views.spec.ts --project=chromium-desktop",
|
||||
"test:e2e:ci": "node scripts/run-playwright-ci-parallel.mjs",
|
||||
"test:e2e:full:slice": "node scripts/run-playwright-full-slice.mjs",
|
||||
@@ -30,7 +35,7 @@
|
||||
"test:e2e:live": "playwright test --config=playwright.live.config.ts",
|
||||
"test:e2e:release": "npm run test:e2e:prod && npm run test:e2e:live",
|
||||
"test:e2e:pos-mobile-live": "node -e \"const { spawnSync } = require('child_process'); const result = spawnSync('npx', ['playwright', 'test', 'tests/e2e/adminModulePosMobileOrderFlow.spec.ts', '--project=chromium-mobile'], { stdio: 'inherit', shell: true, env: { ...process.env, PLAYWRIGHT_LIVE: '1' } }); process.exit(result.status ?? 1);\"",
|
||||
"test:all": "npm run test:unit && npm run test:e2e:smoke",
|
||||
"test:all": "npm run test:unit && npm run test:e2e:pr",
|
||||
"twa:build": "bubblewrap build",
|
||||
"twa:update": "bubblewrap update"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
export const chromiumProjects = ["chromium-desktop", "chromium-mobile"];
|
||||
export const prGrep = "@pr";
|
||||
export const smokeGrep = "@smoke";
|
||||
|
||||
export const fallbackChangePatterns = [
|
||||
/^package(?:-lock)?\.json$/u,
|
||||
/^vite\.config\.js$/u,
|
||||
/^playwright(?:\..+)?\.config\.(?:js|ts)$/u,
|
||||
/^playwright\.global-(?:setup|teardown)\.mjs$/u,
|
||||
/^scripts\/run-playwright/u,
|
||||
/^tests\/e2e\/(?:support|fixtures)\//u,
|
||||
];
|
||||
|
||||
export const sourceMappings = [
|
||||
{
|
||||
name: "auth",
|
||||
patterns: [/^src\/(?:views|components|middleware)\/.*auth/iu, /^src\/views\/auth\//u, /^src\/components\/session\//u],
|
||||
specs: ["tests/e2e/auth.smoke.spec.js", "tests/e2e/userAuth.spec.ts"],
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
{
|
||||
name: "public-routes",
|
||||
patterns: [/^src\/views\/pages\//u, /^src\/components\/page\//u, /^src\/router\.js$/u],
|
||||
specs: ["tests/e2e/navigation.smoke.spec.js"],
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
{
|
||||
name: "booking",
|
||||
patterns: [/bookings?/iu, /time-bookings/iu, /^src\/views\/guest\/book\//u],
|
||||
specs: [
|
||||
"tests/e2e/booking-selfserve.smoke.spec.js",
|
||||
"tests/e2e/userBookWash.spec.ts",
|
||||
"tests/e2e/userBookings.spec.ts",
|
||||
],
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
{
|
||||
name: "pos",
|
||||
patterns: [/\/pos[/-]/iu, /POS/iu, /^src\/assets\/pos\.css$/u],
|
||||
specs: ["tests/e2e/pos-flow.spec.js", "tests/e2e/pos-mobile-order-flow.spec.js", "tests/e2e/admin-pos-orders.spec.ts"],
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
{
|
||||
name: "edge-gateways",
|
||||
patterns: [/edge[-/]?gateways?/iu, /edgeGateway/iu],
|
||||
specs: ["tests/e2e/edge-gateways.smoke.spec.js", "tests/e2e/edge-gateways.routes.spec.js"],
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
{
|
||||
name: "invoicing",
|
||||
patterns: [/invoic/iu, /economic[-/]?queue/iu, /collected-order/iu],
|
||||
specs: [
|
||||
"tests/e2e/invoicing-period.smoke.spec.js",
|
||||
"tests/e2e/invoice-distribution.smoke.spec.js",
|
||||
"tests/e2e/invoice-transfer-queue-history.spec.js",
|
||||
],
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
{
|
||||
name: "workfeed",
|
||||
patterns: [/workfeed/iu],
|
||||
specs: ["tests/e2e/workfeed-config.smoke.spec.js"],
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
{
|
||||
name: "system-status",
|
||||
patterns: [/system[-/]?status/iu, /systemDatabase/iu, /replication/iu],
|
||||
specs: ["tests/e2e/superuser-system-status.smoke.spec.js"],
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
{
|
||||
name: "self-serve",
|
||||
patterns: [/self[-/]?serve/iu, /selfserve/iu, /wash\/MyWash/iu],
|
||||
specs: ["tests/e2e/self-serve-wash.spec.js", "tests/e2e/self-serve-studio-flow.spec.js"],
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
{
|
||||
name: "i18n",
|
||||
patterns: [/^src\/i18n\//u, /i18n/iu, /locales/iu],
|
||||
specs: ["tests/e2e/i18n.smoke.spec.ts"],
|
||||
projects: chromiumProjects,
|
||||
},
|
||||
];
|
||||
@@ -18,6 +18,7 @@ const ownedFilesByRole = {
|
||||
"i18n.smoke.spec.ts",
|
||||
"i18n.views.spec.ts",
|
||||
"navigation.smoke.spec.js",
|
||||
"qr-new-customer-layout.spec.ts",
|
||||
"self-serve-wash.spec.js",
|
||||
"user-orders.spec.ts",
|
||||
"userBookings.spec.ts",
|
||||
@@ -61,16 +62,27 @@ const ownedFilesByRole = {
|
||||
"pos.visual.spec.js",
|
||||
],
|
||||
superuser: [
|
||||
"edge-gateways.fleet-outline.spec.js",
|
||||
"edge-gateways.routes.spec.js",
|
||||
"edge-gateways.smoke.spec.js",
|
||||
"edge-gateways.visual.spec.js",
|
||||
"invoice-distribution.smoke.spec.js",
|
||||
"invoice-transfer-monitor.spec.ts",
|
||||
"invoice-transfer-queue-history.spec.js",
|
||||
"invoicing-period.smoke.spec.js",
|
||||
"issue-repro-duplicates-date.spec.js",
|
||||
"self-serve-sessions.spec.js",
|
||||
"self-serve-studio-audit-navigation.spec.js",
|
||||
"self-serve-studio-flow.spec.js",
|
||||
"superuser-bookings.spec.ts",
|
||||
"superuser-customer-complaints.spec.ts",
|
||||
"superuser-customers-mass-import.spec.ts",
|
||||
"superuser-department-branding.spec.js",
|
||||
"superuser-department-gates.spec.ts",
|
||||
"superuser-department-lanes.spec.ts",
|
||||
"superuser-departments-archive.spec.ts",
|
||||
"superuser-drafts.spec.ts",
|
||||
"superuser-products-layout.spec.ts",
|
||||
"superuser-system-status.smoke.spec.js",
|
||||
"superuser-vehicles.smoke.spec.js",
|
||||
"workfeed-config.smoke.spec.js",
|
||||
|
||||
@@ -0,0 +1,429 @@
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { chromiumProjects, fallbackChangePatterns, prGrep, smokeGrep, sourceMappings } from "./playwright-pr-mapping.mjs";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const workingDirectory = process.cwd();
|
||||
const playwrightCliPath = path.join(workingDirectory, "node_modules", "@playwright", "test", "cli.js");
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const activeChildren = new Set();
|
||||
let isShuttingDown = false;
|
||||
|
||||
for (const signal of ["SIGINT", "SIGTERM"]) {
|
||||
process.on(signal, () => {
|
||||
void shutdown(signal).finally(() => {
|
||||
process.exit(130);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function parseArgs(rawArgs) {
|
||||
const parsed = {
|
||||
help: false,
|
||||
changedOnly: false,
|
||||
listOnly: false,
|
||||
base: "",
|
||||
head: "",
|
||||
projects: [],
|
||||
forwardedArgs: [],
|
||||
};
|
||||
|
||||
for (let index = 0; index < rawArgs.length; index += 1) {
|
||||
const value = rawArgs[index];
|
||||
|
||||
if (value === "--") {
|
||||
parsed.forwardedArgs.push(...rawArgs.slice(index + 1));
|
||||
break;
|
||||
}
|
||||
|
||||
if (value === "--help" || value === "-h") {
|
||||
parsed.help = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value === "--changed-only") {
|
||||
parsed.changedOnly = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value === "--list-only") {
|
||||
parsed.listOnly = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value === "--base" && rawArgs[index + 1] !== undefined) {
|
||||
parsed.base = rawArgs[index + 1];
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value.startsWith("--base=")) {
|
||||
parsed.base = value.slice("--base=".length);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value === "--head" && rawArgs[index + 1] !== undefined) {
|
||||
parsed.head = rawArgs[index + 1];
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value.startsWith("--head=")) {
|
||||
parsed.head = value.slice("--head=".length);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value === "--project" && rawArgs[index + 1] !== undefined) {
|
||||
parsed.projects.push(rawArgs[index + 1]);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value.startsWith("--project=")) {
|
||||
parsed.projects.push(value.slice("--project=".length));
|
||||
continue;
|
||||
}
|
||||
|
||||
parsed.forwardedArgs.push(value);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function printUsage() {
|
||||
console.log(`
|
||||
Playwright PR runner
|
||||
|
||||
Usage:
|
||||
node scripts/run-playwright-pr.mjs [options] [-- <playwright args>]
|
||||
|
||||
Options:
|
||||
--base <ref> Base ref for changed-area detection
|
||||
--head <ref> Head ref for changed-area detection. Default: HEAD
|
||||
--project <name> Restrict to one Chromium Playwright project. Can be repeated.
|
||||
--changed-only Run changed-area selection without the core @pr gate
|
||||
--list-only List selected tests instead of running them
|
||||
-h, --help Show help
|
||||
|
||||
Examples:
|
||||
npm run test:e2e:pr
|
||||
npm run test:e2e:changed -- --base=HEAD~1 --head=HEAD
|
||||
`);
|
||||
}
|
||||
|
||||
function normalizePath(value) {
|
||||
return value.replace(/\\/g, "/").replace(/^\.\//u, "");
|
||||
}
|
||||
|
||||
function isZeroSha(value) {
|
||||
return /^0{40}$/u.test(value || "");
|
||||
}
|
||||
|
||||
function getSelectedProjects(projects = args.projects) {
|
||||
return projects.length > 0 ? projects : chromiumProjects;
|
||||
}
|
||||
|
||||
function unique(values) {
|
||||
return [...new Set(values)];
|
||||
}
|
||||
|
||||
function sanitizeNamespace(value) {
|
||||
return value.replace(/[^a-zA-Z0-9._-]+/g, "-");
|
||||
}
|
||||
|
||||
function getArtifactNamespace(suffix) {
|
||||
const baseNamespace = process.env.PLAYWRIGHT_ARTIFACT_NAMESPACE || "pr";
|
||||
return sanitizeNamespace(`${baseNamespace}-${suffix}`);
|
||||
}
|
||||
|
||||
function buildProjectArgs(projects) {
|
||||
return projects.flatMap((project) => ["--project", project]);
|
||||
}
|
||||
|
||||
async function runPlaywright({ label, commandArgs, artifactSuffix }) {
|
||||
const finalArgs = ["test", ...commandArgs, ...args.forwardedArgs];
|
||||
|
||||
if (args.listOnly && !finalArgs.includes("--list")) {
|
||||
finalArgs.push("--list");
|
||||
}
|
||||
|
||||
console.log(`[playwright-pr] ${args.listOnly ? "Listing" : "Running"} ${label}: playwright ${finalArgs.join(" ")}`);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(process.execPath, [playwrightCliPath, ...finalArgs], {
|
||||
cwd: workingDirectory,
|
||||
env: {
|
||||
...process.env,
|
||||
PLAYWRIGHT: "1",
|
||||
PLAYWRIGHT_ARTIFACT_NAMESPACE: getArtifactNamespace(artifactSuffix),
|
||||
PLAYWRIGHT_REPORTER_MODE: "line-html",
|
||||
},
|
||||
stdio: "inherit",
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
activeChildren.add(child);
|
||||
child.on("close", (code) => {
|
||||
activeChildren.delete(child);
|
||||
resolve(code ?? 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function tryGitDiff(rangeArgs) {
|
||||
const { stdout } = await execFileAsync("git", ["diff", "--name-only", "--diff-filter=ACMR", ...rangeArgs], {
|
||||
cwd: workingDirectory,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
});
|
||||
|
||||
return stdout
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => normalizePath(line.trim()))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
async function getChangedFiles() {
|
||||
const base = args.base.trim();
|
||||
const head = args.head.trim() || "HEAD";
|
||||
|
||||
if (base && !isZeroSha(base)) {
|
||||
try {
|
||||
return {
|
||||
files: await tryGitDiff([`${base}...${head}`]),
|
||||
source: `${base}...${head}`,
|
||||
};
|
||||
} catch {
|
||||
try {
|
||||
return {
|
||||
files: await tryGitDiff([`${base}..${head}`]),
|
||||
source: `${base}..${head}`,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
files: [],
|
||||
source: `${base}...${head}`,
|
||||
unavailable: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
files: await tryGitDiff(["HEAD~1...HEAD"]),
|
||||
source: "HEAD~1...HEAD",
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
files: [],
|
||||
source: "HEAD~1...HEAD",
|
||||
unavailable: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function addSpec(selection, spec, projects) {
|
||||
const normalizedSpec = normalizePath(spec);
|
||||
const existingProjects = selection.specProjects.get(normalizedSpec) || new Set();
|
||||
|
||||
for (const project of projects) {
|
||||
existingProjects.add(project);
|
||||
}
|
||||
|
||||
selection.specProjects.set(normalizedSpec, existingProjects);
|
||||
}
|
||||
|
||||
function isE2eSpec(file) {
|
||||
return /^tests\/e2e\/.+\.spec\.(?:js|ts)$/u.test(file);
|
||||
}
|
||||
|
||||
function shouldFallback(file) {
|
||||
return fallbackChangePatterns.some((pattern) => pattern.test(file));
|
||||
}
|
||||
|
||||
function isFrontendSource(file) {
|
||||
return /^src\//u.test(file) || /^public\//u.test(file) || /^index\.html$/u.test(file);
|
||||
}
|
||||
|
||||
function selectChangedTests(changedFiles) {
|
||||
const selection = {
|
||||
specProjects: new Map(),
|
||||
mappedFiles: [],
|
||||
unmappedFiles: [],
|
||||
fallback: false,
|
||||
};
|
||||
|
||||
const selectedProjects = getSelectedProjects();
|
||||
|
||||
for (const rawFile of changedFiles) {
|
||||
const file = normalizePath(rawFile);
|
||||
|
||||
if (isE2eSpec(file)) {
|
||||
addSpec(selection, file, selectedProjects);
|
||||
selection.mappedFiles.push(file);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (shouldFallback(file)) {
|
||||
selection.fallback = true;
|
||||
selection.unmappedFiles.push(file);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isFrontendSource(file)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const matches = sourceMappings.filter((mapping) => mapping.patterns.some((pattern) => pattern.test(file)));
|
||||
|
||||
if (matches.length === 0) {
|
||||
selection.fallback = true;
|
||||
selection.unmappedFiles.push(file);
|
||||
continue;
|
||||
}
|
||||
|
||||
selection.mappedFiles.push(file);
|
||||
for (const mapping of matches) {
|
||||
const projects = mapping.projects.filter((project) => selectedProjects.includes(project));
|
||||
for (const spec of mapping.specs) {
|
||||
addSpec(selection, spec, projects.length > 0 ? projects : selectedProjects);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return selection;
|
||||
}
|
||||
|
||||
function groupSpecsByProjects(specProjects) {
|
||||
const groups = new Map();
|
||||
|
||||
for (const [spec, projects] of specProjects.entries()) {
|
||||
const projectList = unique([...projects]).sort();
|
||||
const key = projectList.join(",");
|
||||
const existing = groups.get(key) || {
|
||||
projects: projectList,
|
||||
specs: [],
|
||||
};
|
||||
existing.specs.push(spec);
|
||||
groups.set(key, existing);
|
||||
}
|
||||
|
||||
return [...groups.values()].map((group) => ({
|
||||
...group,
|
||||
specs: group.specs.sort(),
|
||||
}));
|
||||
}
|
||||
|
||||
async function runCorePrGate() {
|
||||
const projects = getSelectedProjects();
|
||||
return runPlaywright({
|
||||
label: `core ${prGrep} gate`,
|
||||
artifactSuffix: "core",
|
||||
commandArgs: ["--grep", prGrep, ...buildProjectArgs(projects)],
|
||||
});
|
||||
}
|
||||
|
||||
async function runChangedSelection(selection) {
|
||||
const projects = getSelectedProjects();
|
||||
|
||||
if (selection.fallback) {
|
||||
console.log(
|
||||
`[playwright-pr] Falling back to broader ${smokeGrep} coverage because these changed files were unmapped: ${selection.unmappedFiles.join(", ")}`
|
||||
);
|
||||
return runPlaywright({
|
||||
label: `fallback ${smokeGrep} gate`,
|
||||
artifactSuffix: "smoke-fallback",
|
||||
commandArgs: ["--grep", smokeGrep, "--grep-invert", prGrep, ...buildProjectArgs(projects)],
|
||||
});
|
||||
}
|
||||
|
||||
const groups = groupSpecsByProjects(selection.specProjects);
|
||||
if (groups.length === 0) {
|
||||
console.log("[playwright-pr] No changed-area E2E specs selected.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (const [index, group] of groups.entries()) {
|
||||
const code = await runPlaywright({
|
||||
label: `changed-area specs ${index + 1}/${groups.length}`,
|
||||
artifactSuffix: `changed-${index + 1}`,
|
||||
commandArgs: [...group.specs, ...buildProjectArgs(group.projects)],
|
||||
});
|
||||
|
||||
if (code !== 0) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function shutdown(signal) {
|
||||
if (isShuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
isShuttingDown = true;
|
||||
console.error(`Received ${signal}. Stopping active Playwright PR children...`);
|
||||
|
||||
await Promise.all(
|
||||
[...activeChildren].map(
|
||||
(child) =>
|
||||
new Promise((resolve) => {
|
||||
if (!child.pid) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.platform === "win32") {
|
||||
const killer = spawn("taskkill", ["/PID", String(child.pid), "/T", "/F"], {
|
||||
stdio: "ignore",
|
||||
windowsHide: true,
|
||||
});
|
||||
killer.on("close", resolve);
|
||||
return;
|
||||
}
|
||||
|
||||
child.kill("SIGTERM");
|
||||
resolve();
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (args.help) {
|
||||
printUsage();
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.access(playwrightCliPath);
|
||||
|
||||
if (!args.changedOnly) {
|
||||
const coreCode = await runCorePrGate();
|
||||
if (coreCode !== 0) {
|
||||
process.exitCode = coreCode;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const changed = await getChangedFiles();
|
||||
if (changed.unavailable) {
|
||||
console.log(`[playwright-pr] Changed-area diff unavailable for ${changed.source}; skipping changed-area selection.`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[playwright-pr] Changed-area diff ${changed.source}: ${changed.files.length} file(s).`);
|
||||
const selection = selectChangedTests(changed.files);
|
||||
const changedCode = await runChangedSelection(selection);
|
||||
if (changedCode !== 0) {
|
||||
process.exitCode = changedCode;
|
||||
}
|
||||
}
|
||||
|
||||
await main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
const workingDirectory = process.cwd();
|
||||
const workingDirectory = await fs.realpath(process.cwd());
|
||||
const vitestCliPath = path.join(workingDirectory, "node_modules", "vitest", "vitest.mjs");
|
||||
const unitTestsRoot = path.join(workingDirectory, "tests", "unit");
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
@@ -23,6 +23,7 @@ function parseArgs(rawArgs) {
|
||||
const parsed = {
|
||||
help: false,
|
||||
batchSize: null,
|
||||
fromList: null,
|
||||
specFilters: [],
|
||||
forwardedArgs: [],
|
||||
};
|
||||
@@ -51,6 +52,17 @@ function parseArgs(rawArgs) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value === "--from-list" && rawArgs[index + 1]) {
|
||||
parsed.fromList = rawArgs[index + 1];
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value.startsWith("--from-list=")) {
|
||||
parsed.fromList = value.slice("--from-list=".length);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value.startsWith("-")) {
|
||||
parsed.forwardedArgs.push(...rawArgs.slice(index));
|
||||
break;
|
||||
@@ -80,15 +92,31 @@ Usage:
|
||||
|
||||
Options:
|
||||
--batch-size <n> Spec files per batch. Default: 5
|
||||
--from-list <file> Read additional spec filters from a newline-delimited file
|
||||
-h, --help Show help
|
||||
|
||||
Examples:
|
||||
npm run test:unit
|
||||
npm run test:unit:serial
|
||||
npm run test:unit -- tests/unit/select-vehicle-form-pos.spec.js
|
||||
npm run test:unit -- tests/unit/select-vehicle-form-pos.spec.js -- --reporter=verbose
|
||||
`);
|
||||
}
|
||||
|
||||
async function readSpecFilterList(listFile) {
|
||||
if (!listFile) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const listPath = path.isAbsolute(listFile) ? listFile : path.join(workingDirectory, listFile);
|
||||
const contents = await fs.readFile(listPath, "utf8");
|
||||
|
||||
return contents
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => line.replace(/#.*/u, "").trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
async function collectSpecFiles(targetDirectory) {
|
||||
const directoryEntries = await fs.readdir(targetDirectory, { withFileTypes: true });
|
||||
const files = [];
|
||||
@@ -234,7 +262,8 @@ async function main() {
|
||||
await fs.access(vitestCliPath);
|
||||
|
||||
const discoveredFiles = await collectSpecFiles(unitTestsRoot);
|
||||
const selectedFiles = applyFilters(discoveredFiles, args.specFilters);
|
||||
const listFilters = await readSpecFilterList(args.fromList);
|
||||
const selectedFiles = applyFilters(discoveredFiles, [...listFilters, ...args.specFilters]);
|
||||
|
||||
if (selectedFiles.length === 0) {
|
||||
throw new Error("No unit spec files matched the provided filters.");
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
const workingDirectory = await fs.realpath(process.cwd());
|
||||
const vitestCliPath = path.join(workingDirectory, "node_modules", "vitest", "vitest.mjs");
|
||||
const unitTestsRoot = path.join(workingDirectory, "tests", "unit");
|
||||
const serialListPath = path.join(unitTestsRoot, "serial-tests.txt");
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
let activeChild = null;
|
||||
let isShuttingDown = false;
|
||||
|
||||
for (const signal of ["SIGINT", "SIGTERM"]) {
|
||||
process.on(signal, () => {
|
||||
void shutdown(signal).finally(() => {
|
||||
process.exit(130);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function parseArgs(rawArgs) {
|
||||
const parsed = {
|
||||
help: false,
|
||||
specFilters: [],
|
||||
forwardedArgs: [],
|
||||
};
|
||||
|
||||
for (let index = 0; index < rawArgs.length; index += 1) {
|
||||
const value = rawArgs[index];
|
||||
|
||||
if (value === "--") {
|
||||
parsed.forwardedArgs.push(...rawArgs.slice(index + 1));
|
||||
break;
|
||||
}
|
||||
|
||||
if (value === "--help" || value === "-h") {
|
||||
parsed.help = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value.startsWith("-")) {
|
||||
parsed.forwardedArgs.push(...rawArgs.slice(index));
|
||||
break;
|
||||
}
|
||||
|
||||
parsed.specFilters.push(value);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function normalizePathSegments(value) {
|
||||
return value.replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
function printUsage() {
|
||||
console.log(`
|
||||
Vitest fast unit runner
|
||||
|
||||
Usage:
|
||||
node scripts/run-vitest-unit-fast.mjs [spec-filter ...] [-- <vitest args>]
|
||||
|
||||
Runs all unit specs except entries in tests/unit/serial-tests.txt.
|
||||
|
||||
Environment:
|
||||
VITEST_FAST_MAX_WORKERS Optional max workers for the fast pass
|
||||
|
||||
Examples:
|
||||
npm run test:unit:fast
|
||||
npm run test:unit:fast -- tests/unit/workfeed
|
||||
npm run test:unit:fast -- --reporter=verbose
|
||||
`);
|
||||
}
|
||||
|
||||
async function collectSpecFiles(targetDirectory) {
|
||||
const directoryEntries = await fs.readdir(targetDirectory, { withFileTypes: true });
|
||||
const files = [];
|
||||
|
||||
for (const entry of directoryEntries) {
|
||||
const absolutePath = path.join(targetDirectory, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...(await collectSpecFiles(absolutePath)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isFile() && entry.name.endsWith(".spec.js")) {
|
||||
files.push(absolutePath);
|
||||
}
|
||||
}
|
||||
|
||||
return files.sort((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
async function readSerialAllowlist() {
|
||||
const contents = await fs.readFile(serialListPath, "utf8");
|
||||
|
||||
return new Set(
|
||||
contents
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => line.replace(/#.*/u, "").trim())
|
||||
.filter(Boolean)
|
||||
.map(normalizePathSegments)
|
||||
);
|
||||
}
|
||||
|
||||
function matchesFilter(relativePath, rawFilter) {
|
||||
const normalizedRelativePath = normalizePathSegments(relativePath);
|
||||
const normalizedFilter = normalizePathSegments(rawFilter.trim());
|
||||
|
||||
if (!normalizedFilter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
normalizedRelativePath === normalizedFilter ||
|
||||
normalizedRelativePath.endsWith(normalizedFilter) ||
|
||||
normalizedRelativePath.includes(normalizedFilter)
|
||||
);
|
||||
}
|
||||
|
||||
function applyFilters(files, specFilters) {
|
||||
if (specFilters.length === 0) {
|
||||
return files;
|
||||
}
|
||||
|
||||
return files.filter((absolutePath) => {
|
||||
const relativePath = path.relative(workingDirectory, absolutePath);
|
||||
return specFilters.some((filter) => matchesFilter(relativePath, filter));
|
||||
});
|
||||
}
|
||||
|
||||
function hasForwardedArg(name) {
|
||||
return args.forwardedArgs.some((value) => value === name || value.startsWith(`${name}=`));
|
||||
}
|
||||
|
||||
function resolveMaxWorkers() {
|
||||
return (process.env.VITEST_FAST_MAX_WORKERS || "").trim();
|
||||
}
|
||||
|
||||
async function runVitest(files) {
|
||||
const enforcedArgs = [];
|
||||
|
||||
const configuredMaxWorkers = resolveMaxWorkers();
|
||||
if (configuredMaxWorkers && !hasForwardedArg("--maxWorkers")) {
|
||||
enforcedArgs.push(`--maxWorkers=${configuredMaxWorkers}`);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
activeChild = spawn(process.execPath, [vitestCliPath, "run", ...files, ...enforcedArgs, ...args.forwardedArgs], {
|
||||
cwd: workingDirectory,
|
||||
env: process.env,
|
||||
stdio: "inherit",
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
activeChild.on("close", (code) => {
|
||||
activeChild = null;
|
||||
resolve(code ?? 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function shutdown(signal) {
|
||||
if (isShuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
isShuttingDown = true;
|
||||
console.error(`Received ${signal}. Stopping active Vitest fast run...`);
|
||||
|
||||
if (activeChild?.pid) {
|
||||
if (process.platform === "win32") {
|
||||
spawn("taskkill", ["/PID", String(activeChild.pid), "/T", "/F"], {
|
||||
stdio: "ignore",
|
||||
windowsHide: true,
|
||||
});
|
||||
} else {
|
||||
activeChild.kill("SIGTERM");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (args.help) {
|
||||
printUsage();
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.access(vitestCliPath);
|
||||
|
||||
const serialAllowlist = await readSerialAllowlist();
|
||||
const discoveredFiles = await collectSpecFiles(unitTestsRoot);
|
||||
const fastFiles = discoveredFiles.filter((absolutePath) => {
|
||||
const relativePath = normalizePathSegments(path.relative(workingDirectory, absolutePath));
|
||||
return !serialAllowlist.has(relativePath);
|
||||
});
|
||||
const selectedFiles = applyFilters(fastFiles, args.specFilters);
|
||||
|
||||
if (selectedFiles.length === 0) {
|
||||
console.log("No parallel-safe unit spec files matched the provided filters.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Running ${selectedFiles.length} parallel-safe unit spec file(s); ${serialAllowlist.size} spec file(s) remain in the serial allowlist.`
|
||||
);
|
||||
|
||||
const code = await runVitest(selectedFiles);
|
||||
if (code !== 0) {
|
||||
process.exitCode = code;
|
||||
}
|
||||
}
|
||||
|
||||
await main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -240,7 +240,7 @@ const getProductOptionsLabel = (vehicle) => {
|
||||
v-if="!props.compact"
|
||||
:user_id="object.user_id"
|
||||
:reg_1="object.reg"
|
||||
:displayActionsDirectly="false"
|
||||
:displayActionsDirectly="true"
|
||||
>
|
||||
<template #actions>
|
||||
<!-- View (Redirect to the vehicle page) -->
|
||||
|
||||
@@ -6,7 +6,7 @@ const AUTH_ENTRY_TIMEOUT = 60_000;
|
||||
test.describe("Auth entry smoke", () => {
|
||||
test.describe.configure({ timeout: 90_000 });
|
||||
|
||||
test("@smoke protected routes redirect to login without token", async ({ page }) => {
|
||||
test("@smoke @pr protected routes redirect to login without token", async ({ page }) => {
|
||||
await mockApi(page);
|
||||
await page.goto("/user");
|
||||
await expect(page).toHaveURL(/\/login/, { timeout: AUTH_ENTRY_TIMEOUT });
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect, test } from "@playwright/test";
|
||||
import { mockApi } from "./support/network.js";
|
||||
|
||||
test.describe("Booking/self-serve smoke", () => {
|
||||
test("@smoke guest booking route loads and keeps key controls available", async ({ page }) => {
|
||||
test("@smoke @pr guest booking route loads and keeps key controls available", async ({ page }) => {
|
||||
await mockApi(page);
|
||||
await page.goto("/guest/book/wash", { waitUntil: "domcontentloaded" });
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ async function acceleratePageTimers(page, timerScale = 0.01) {
|
||||
test.describe("Edge gateway management smoke", () => {
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
test("@smoke shows technical request details for backend errors", async ({ page }) => {
|
||||
test("@smoke @pr shows technical request details for backend errors", async ({ page }) => {
|
||||
await mockApi(page, {
|
||||
authenticated: true,
|
||||
permissions: ["superuser", "user"],
|
||||
|
||||
@@ -76,7 +76,7 @@ function collectSubstringPaths(value: unknown, substring: string, prefix = ""):
|
||||
}
|
||||
|
||||
test.describe("Danish locale smoke", () => {
|
||||
test("@smoke normalizes targeted Danish locale copy in da locale", async ({ request }) => {
|
||||
test("@smoke @pr normalizes targeted Danish locale copy in da locale", async ({ request }) => {
|
||||
const response = await request.get("/src/i18n/locales/da.json");
|
||||
expect(response.ok()).toBe(true);
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ async function prepareInvoiceDistributionPage(page, overrides = {}) {
|
||||
}
|
||||
|
||||
test.describe("Invoice distribution smoke", () => {
|
||||
test("@smoke overview loads and quick-open month action works", async ({ page }) => {
|
||||
test("@smoke @pr overview loads and quick-open month action works", async ({ page }) => {
|
||||
const componentWarnings = [];
|
||||
page.on("console", (message) => {
|
||||
const text = message.text();
|
||||
|
||||
@@ -910,7 +910,7 @@ function expectBoxInside(innerBox, outerBox, label, tolerance = 1) {
|
||||
}
|
||||
|
||||
test.describe("Invoicing period tab", () => {
|
||||
test("@smoke period view does not throw queue refresh errors on load", async ({ page }) => {
|
||||
test("@smoke @pr period view does not throw queue refresh errors on load", async ({ page }) => {
|
||||
const pageErrors = [];
|
||||
page.on("pageerror", (error) => {
|
||||
pageErrors.push(error.message);
|
||||
|
||||
@@ -6,7 +6,7 @@ test.describe("Public route smoke", () => {
|
||||
await mockApi(page);
|
||||
});
|
||||
|
||||
test("@smoke app boot loads landing page", async ({ page }) => {
|
||||
test("@smoke @pr app boot loads landing page", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page).toHaveURL(/\/$/);
|
||||
await expect(page.getByTestId("landing-title")).toContainText("Truck Wash");
|
||||
|
||||
@@ -1294,7 +1294,7 @@ test.describe("POS mobile order flow", () => {
|
||||
await expect(page.getByTestId("pos-mobile-step-1-reference-warning-icon")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("@smoke manual input happy path creates, completes, and resets", async ({ page }) => {
|
||||
test("@smoke @pr manual input happy path creates, completes, and resets", async ({ page }) => {
|
||||
const fixture = createMobilePosFixture();
|
||||
await createOrderFromStep1(page, fixture, {
|
||||
reg: "ZZ00000",
|
||||
|
||||
@@ -501,7 +501,7 @@ const longTitleSnapshot = {
|
||||
};
|
||||
|
||||
test.describe("Superuser system status smoke", () => {
|
||||
test("@smoke superuser status dashboard renders Danish translations for degraded infrastructure and recent sessions", async ({
|
||||
test("@smoke @pr superuser status dashboard renders Danish translations for degraded infrastructure and recent sessions", async ({
|
||||
page,
|
||||
}) => {
|
||||
await bootSuperuser(page);
|
||||
|
||||
@@ -50,7 +50,7 @@ test.describe("Workfeed configuration smoke", () => {
|
||||
await primeSuperuserSession(page);
|
||||
});
|
||||
|
||||
test("@smoke renders workfeed config page and runs diagnostics", async ({ page }) => {
|
||||
test("@smoke @pr renders workfeed config page and runs diagnostics", async ({ page }) => {
|
||||
await page.goto("/superuser/configuration/workfeed");
|
||||
|
||||
await expect(page).toHaveURL(/\/superuser\/configuration\/workfeed$/);
|
||||
|
||||
@@ -273,6 +273,7 @@ describe("Invoicing period queue-driven refresh", () => {
|
||||
page: 1,
|
||||
limit: 100,
|
||||
search: "",
|
||||
flagTab: "all",
|
||||
includeRequiresAction: 1,
|
||||
includeBooked: 1,
|
||||
},
|
||||
@@ -338,6 +339,7 @@ describe("Invoicing period queue-driven refresh", () => {
|
||||
page: 1,
|
||||
limit: 100,
|
||||
search: "",
|
||||
flagTab: "all",
|
||||
includeRequiresAction: 1,
|
||||
includeBooked: 1,
|
||||
},
|
||||
@@ -393,6 +395,7 @@ describe("Invoicing period queue-driven refresh", () => {
|
||||
page: 2,
|
||||
limit: 100,
|
||||
search: "",
|
||||
flagTab: "all",
|
||||
includeRequiresAction: 1,
|
||||
includeBooked: 1,
|
||||
periodWarm: 1,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Specs in this allowlist mutate shared browser state, fake timers, storage, or process globals.
|
||||
# Keep this list small; new unit specs default to the parallel fast pass.
|
||||
tests/unit/action-settings-wheel-button.spec.js
|
||||
tests/unit/authenticated-request.spec.js
|
||||
tests/unit/collected-order-invoices-closed-at-modal.spec.js
|
||||
tests/unit/customer-search-field-pos.spec.js
|
||||
tests/unit/department-daily-report-complaints.spec.js
|
||||
tests/unit/department-gates-config-modal.spec.js
|
||||
tests/unit/department-lanes-relay-options.spec.js
|
||||
tests/unit/economic-transfer-queue.spec.js
|
||||
tests/unit/edge-gateway-service.spec.js
|
||||
tests/unit/invoicing-period-queue-refresh.behavior.spec.js
|
||||
tests/unit/invoicing-period-queue-state.behavior.spec.js
|
||||
tests/unit/orders-change-customer.spec.js
|
||||
tests/unit/orders-items.spec.js
|
||||
tests/unit/orders-table.spec.js
|
||||
tests/unit/origin-migration.spec.js
|
||||
tests/unit/paginated-list-export.spec.js
|
||||
tests/unit/pos-department-process.spec.js
|
||||
tests/unit/pos-last-scanned-license-plates.spec.js
|
||||
tests/unit/request-queue-progress.spec.js
|
||||
tests/unit/select-vehicle-form-pos.spec.js
|
||||
tests/unit/self-serve-machine-connectivity.spec.js
|
||||
tests/unit/session-token-initialization-contract.spec.js
|
||||
tests/unit/superuser-invoices-view.spec.js
|
||||
tests/unit/superuser-system-status-dashboard.spec.js
|
||||
tests/unit/system-search-modal-contract.spec.js
|
||||
tests/unit/user-booking-date-time-selector.spec.js
|
||||
tests/unit/use-wash-progress.spec.js
|
||||
tests/unit/xlvask-usage-amount-cache.spec.js
|
||||
+112
-1
@@ -1 +1,112 @@
|
||||
// Intentionally empty. The shared Vitest config expects this file to exist.
|
||||
import { afterEach, beforeEach, vi } from "vitest";
|
||||
import { enableAutoUnmount } from "@vue/test-utils";
|
||||
|
||||
enableAutoUnmount(afterEach);
|
||||
|
||||
class TestResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
class TestIntersectionObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
takeRecords() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function createMatchMedia(query) {
|
||||
return {
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener() {},
|
||||
removeListener() {},
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
dispatchEvent() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function ensureBrowserMocks() {
|
||||
if (!globalThis.ResizeObserver) {
|
||||
globalThis.ResizeObserver = TestResizeObserver;
|
||||
}
|
||||
|
||||
if (!globalThis.IntersectionObserver) {
|
||||
globalThis.IntersectionObserver = TestIntersectionObserver;
|
||||
}
|
||||
|
||||
if (globalThis.window) {
|
||||
if (!window.matchMedia) {
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: createMatchMedia,
|
||||
});
|
||||
}
|
||||
|
||||
if (!window.scrollTo) {
|
||||
Object.defineProperty(window, "scrollTo", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value() {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (globalThis.URL) {
|
||||
if (!URL.createObjectURL) {
|
||||
Object.defineProperty(URL, "createObjectURL", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: vi.fn(() => "blob:vitest-object-url"),
|
||||
});
|
||||
}
|
||||
|
||||
if (!URL.revokeObjectURL) {
|
||||
Object.defineProperty(URL, "revokeObjectURL", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: vi.fn(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearStorage(storage) {
|
||||
try {
|
||||
storage?.clear();
|
||||
} catch {
|
||||
// Some jsdom URL modes can make storage unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
ensureBrowserMocks();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
vi.useRealTimers();
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
|
||||
if (globalThis.document?.body) {
|
||||
document.body.innerHTML = "";
|
||||
}
|
||||
|
||||
if (globalThis.window) {
|
||||
clearStorage(window.localStorage);
|
||||
clearStorage(window.sessionStorage);
|
||||
}
|
||||
|
||||
ensureBrowserMocks();
|
||||
});
|
||||
|
||||
@@ -223,6 +223,10 @@ export default defineConfig(({ mode }) => {
|
||||
}
|
||||
}
|
||||
},
|
||||
test: {
|
||||
setupFiles: [fromProjectRoot('tests', 'unit', 'setup.js')],
|
||||
isolate: true
|
||||
},
|
||||
server: {
|
||||
watch: {
|
||||
ignored: ['**/output/playwright/**', '**/node_modules.codex-backup/**']
|
||||
|
||||
Reference in New Issue
Block a user