Add and improve e2e tests for Edge Gateway flows and configure pre-commit hooks:
- Introduced live smoke tests for Edge Gateways, verifying gateway routes and destructive-action prevention. - Added e2e scenarios for deep-link navigation, unavailable gateway recovery, token rotation, and background page refresh. - Refactored test helpers for streamlined functional validation in gateway scenarios. - Updated `EdgeGatewayTerminal.vue` with test IDs for enhanced testability. - Added and configured Husky pre-commit hooks for automated test file formatting and validation.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(scriptDir, "..");
|
||||
const gitPathspecs = [":(glob)tests/**/*.js", ":(glob)tests/**/*.ts"];
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
...options,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
console.error(message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function runNpmScript(scriptName) {
|
||||
if (process.env.npm_execpath) {
|
||||
return run(process.execPath, [process.env.npm_execpath, "run", scriptName], { stdio: "inherit" });
|
||||
}
|
||||
|
||||
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
return run(npmCommand, ["run", scriptName], {
|
||||
shell: process.platform === "win32",
|
||||
stdio: "inherit",
|
||||
});
|
||||
}
|
||||
|
||||
function parseLines(output) {
|
||||
return output
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
const worktreeResult = run("git", ["rev-parse", "--show-toplevel"]);
|
||||
|
||||
if (worktreeResult.status !== 0) {
|
||||
fail("Pre-commit formatting must run inside the front-end-vue Git worktree.");
|
||||
}
|
||||
|
||||
const gitRoot = path.resolve(worktreeResult.stdout.trim());
|
||||
|
||||
if (gitRoot !== repoRoot) {
|
||||
fail(`Expected front-end-vue to be the Git root, but git reported: ${gitRoot}`);
|
||||
}
|
||||
|
||||
const unstagedTrackedResult = run("git", ["diff", "--name-only", "--", ...gitPathspecs]);
|
||||
|
||||
if (unstagedTrackedResult.status !== 0) {
|
||||
fail("Unable to inspect unstaged test-file changes before running Prettier.");
|
||||
}
|
||||
|
||||
const untrackedResult = run("git", ["ls-files", "--others", "--exclude-standard", "--", ...gitPathspecs]);
|
||||
|
||||
if (untrackedResult.status !== 0) {
|
||||
fail("Unable to inspect untracked test files before running Prettier.");
|
||||
}
|
||||
|
||||
const unsafePaths = [
|
||||
...parseLines(unstagedTrackedResult.stdout),
|
||||
...parseLines(untrackedResult.stdout),
|
||||
];
|
||||
|
||||
if (unsafePaths.length > 0) {
|
||||
const formattedPaths = unsafePaths.map((filePath) => ` - ${filePath}`).join("\n");
|
||||
fail(
|
||||
[
|
||||
"Pre-commit formatting aborted because tests contain unstaged changes.",
|
||||
"Stage or discard these files before committing so the hook does not add unrelated work:",
|
||||
formattedPaths,
|
||||
].join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Running Prettier for frontend test files...");
|
||||
const formatResult = runNpmScript("format:tests");
|
||||
|
||||
if (formatResult.status !== 0) {
|
||||
process.exit(formatResult.status ?? 1);
|
||||
}
|
||||
|
||||
console.log("Restaging formatted frontend test files...");
|
||||
const knownFilesResult = run("git", ["ls-files", "--cached", "--others", "--exclude-standard", "--", ...gitPathspecs]);
|
||||
|
||||
if (knownFilesResult.status !== 0) {
|
||||
fail("Prettier completed, but the hook could not resolve test files for restaging.");
|
||||
}
|
||||
|
||||
const filesToStage = parseLines(knownFilesResult.stdout);
|
||||
|
||||
if (filesToStage.length === 0) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const addResult = run("git", ["add", "--", ...filesToStage], { stdio: "inherit" });
|
||||
|
||||
if (addResult.status !== 0) {
|
||||
fail("Prettier completed, but git add failed while restaging formatted test files.");
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(scriptDir, "..");
|
||||
const huskyBin = path.join(repoRoot, "node_modules", "husky", "bin.js");
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
...options,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const worktreeResult = run("git", ["rev-parse", "--show-toplevel"]);
|
||||
|
||||
if (worktreeResult.status !== 0) {
|
||||
console.log("Skipping Husky install because front-end-vue is not attached to a Git worktree.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const gitRoot = path.resolve(worktreeResult.stdout.trim());
|
||||
|
||||
if (gitRoot !== repoRoot) {
|
||||
console.log(`Skipping Husky install because Git root is ${gitRoot}, not ${repoRoot}.`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const huskyResult = run(process.execPath, [huskyBin], { stdio: "inherit" });
|
||||
process.exit(huskyResult.status ?? 1);
|
||||
@@ -8,7 +8,7 @@ const forwardedArgs = process.argv.slice(2);
|
||||
const workingDirectory = process.cwd();
|
||||
const playwrightCliPath = path.join(workingDirectory, "node_modules", "@playwright", "test", "cli.js");
|
||||
const basePort = normalizePositiveInt(process.env.PLAYWRIGHT_PARALLEL_BASE_PORT, 5191);
|
||||
const perProcessWorkers = normalizePositiveInt(process.env.PLAYWRIGHT_PARALLEL_WORKERS, 1);
|
||||
const legacyPerProcessWorkers = parsePositiveInt(process.env.PLAYWRIGHT_PARALLEL_WORKERS);
|
||||
const reportIndexDirectory = path.join(workingDirectory, "output", "playwright", "ci-parallel-report");
|
||||
const execFileAsync = promisify(execFile);
|
||||
const activeChildren = new Set();
|
||||
@@ -18,20 +18,63 @@ const groups = [
|
||||
{
|
||||
name: "chromium",
|
||||
projects: ["chromium-desktop", "chromium-mobile"],
|
||||
defaultWorkers: 2,
|
||||
},
|
||||
{
|
||||
name: "firefox",
|
||||
projects: ["firefox-desktop"],
|
||||
defaultWorkers: 1,
|
||||
},
|
||||
{
|
||||
name: "webkit",
|
||||
projects: ["webkit-desktop", "webkit-mobile"],
|
||||
defaultWorkers: 2,
|
||||
},
|
||||
];
|
||||
|
||||
function normalizePositiveInt(value, fallback) {
|
||||
function parsePositiveInt(value) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
||||
}
|
||||
|
||||
function normalizePositiveInt(value, fallback) {
|
||||
const parsed = parsePositiveInt(value);
|
||||
return parsed ?? fallback;
|
||||
}
|
||||
|
||||
function resolveGroupWorkers(group) {
|
||||
const envName = `PLAYWRIGHT_PARALLEL_WORKERS_${group.name.toUpperCase()}`;
|
||||
const groupWorkers = parsePositiveInt(process.env[envName]);
|
||||
|
||||
if (groupWorkers !== null) {
|
||||
return groupWorkers;
|
||||
}
|
||||
|
||||
if (legacyPerProcessWorkers !== null) {
|
||||
return legacyPerProcessWorkers;
|
||||
}
|
||||
|
||||
return group.defaultWorkers;
|
||||
}
|
||||
|
||||
function resolveConfiguredGroups() {
|
||||
return groups.map((group) => ({
|
||||
...group,
|
||||
workers: resolveGroupWorkers(group),
|
||||
}));
|
||||
}
|
||||
|
||||
function validateWorkerCap(configuredGroups) {
|
||||
const totalWorkers = configuredGroups.reduce((sum, group) => sum + group.workers, 0);
|
||||
|
||||
if (totalWorkers > 5) {
|
||||
const groupSummary = configuredGroups.map((group) => `${group.name}=${group.workers}`).join(", ");
|
||||
throw new Error(
|
||||
`Parallel Playwright worker cap exceeded (${totalWorkers} > 5). Configure the group workers so their sum stays at or below 5. Current values: ${groupSummary}`
|
||||
);
|
||||
}
|
||||
|
||||
return totalWorkers;
|
||||
}
|
||||
|
||||
function getArtifactNamespace(group) {
|
||||
@@ -89,8 +132,8 @@ function spawnGroup(group, index) {
|
||||
const artifactNamespace = getArtifactNamespace(group);
|
||||
const args = [
|
||||
"test",
|
||||
...group.projects.flatMap((project) => ["--project", project]),
|
||||
...forwardedArgs,
|
||||
...group.projects.flatMap((project) => ["--project", project]),
|
||||
];
|
||||
|
||||
const child = spawn(process.execPath, [playwrightCliPath, ...args], {
|
||||
@@ -99,7 +142,7 @@ function spawnGroup(group, index) {
|
||||
...process.env,
|
||||
PLAYWRIGHT_BASE_URL: "",
|
||||
PLAYWRIGHT_DEV_PORT: String(devPort),
|
||||
PLAYWRIGHT_WORKERS: String(perProcessWorkers),
|
||||
PLAYWRIGHT_WORKERS: String(group.workers),
|
||||
PLAYWRIGHT_ARTIFACT_NAMESPACE: artifactNamespace,
|
||||
PLAYWRIGHT_REPORTER_MODE: "line-html",
|
||||
PLAYWRIGHT: "1",
|
||||
@@ -121,6 +164,7 @@ function spawnGroup(group, index) {
|
||||
artifactNamespace,
|
||||
projects: group.projects,
|
||||
port: devPort,
|
||||
workers: group.workers,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -141,6 +185,7 @@ async function writeCombinedReportIndex(results) {
|
||||
<td>${result.name}</td>
|
||||
<td>${result.projects.join(", ")}</td>
|
||||
<td>${result.port}</td>
|
||||
<td>${result.workers}</td>
|
||||
<td style="color: ${statusColor}; font-weight: 600;">${status}</td>
|
||||
<td><a href="${reportHref}">Open report</a></td>
|
||||
</tr>`;
|
||||
@@ -172,6 +217,7 @@ async function writeCombinedReportIndex(results) {
|
||||
<th>Group</th>
|
||||
<th>Projects</th>
|
||||
<th>Port</th>
|
||||
<th>Workers</th>
|
||||
<th>Status</th>
|
||||
<th>Report</th>
|
||||
</tr>
|
||||
@@ -207,14 +253,17 @@ for (const signal of ["SIGINT", "SIGTERM"]) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const configuredGroups = resolveConfiguredGroups();
|
||||
const totalWorkers = validateWorkerCap(configuredGroups);
|
||||
|
||||
console.log(
|
||||
`Starting Playwright CI in parallel with ${groups.length} processes and ${perProcessWorkers} worker(s) per process.`
|
||||
`Starting Playwright CI in parallel with ${configuredGroups.length} processes and ${totalWorkers} total worker(s): ${configuredGroups.map((group) => `${group.name}=${group.workers}`).join(", ")}.`
|
||||
);
|
||||
|
||||
await Promise.all(groups.map((group) => cleanupDevServerArtifacts(getArtifactNamespace(group))));
|
||||
const results = await Promise.all(groups.map((group, index) => spawnGroup(group, index)));
|
||||
await Promise.all(configuredGroups.map((group) => cleanupDevServerArtifacts(getArtifactNamespace(group))));
|
||||
const results = await Promise.all(configuredGroups.map((group, index) => spawnGroup(group, index)));
|
||||
await writeCombinedReportIndex(results);
|
||||
await Promise.all(groups.map((group) => cleanupDevServerArtifacts(getArtifactNamespace(group))));
|
||||
await Promise.all(configuredGroups.map((group) => cleanupDevServerArtifacts(getArtifactNamespace(group))));
|
||||
|
||||
const failedRuns = results.filter((result) => result.code !== 0);
|
||||
if (failedRuns.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user