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:
Jeppe Bundgaard
2026-04-16 10:52:56 +02:00
parent 95a8f114bc
commit 6c4b9cb599
33 changed files with 1737 additions and 385 deletions
+111
View File
@@ -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.");
}
+38
View File
@@ -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);
+58 -9
View File
@@ -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) {