Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/run-playwright-a11y.mjs
T

339 lines
13 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* SKIP-safe wrapper around `playwright test` for the e2e a11y gate.
*
* Skips (exit 0) when any of the following are true:
* - Playwright runtime is not installed (`npx --no-install playwright --version` fails)
* - Target URL (E2E_BASE_URL or http://localhost:3000) is not reachable
*
* Set REQUIRE_E2E_A11Y=1 in staging/CI lanes where missing browser runtime or
* an unreachable target must fail instead of skipping. Set REQUIRE_AUTH_E2E=1
* when authenticated browser specs must not silently skip. Set
* REQUIRE_E2E_READINESS=1 for staging/readiness lanes where authenticated
* dashboard, governance settings, subscription, portal management, admin email
* sequences, plugin settings, workflow builder, workflow smoke inputs, and
* read-only plugin credentials must be present before Playwright starts. Set
* REQUIRE_E2E_MUTATING_READINESS=1 only
* for disposable readiness lanes where registration/account/plugin mutation
* opt-ins must also be present. Set E2E_REQUIRE_SUBSCRIPTION_SUCCESS=1 only in
* lanes with an externally acquired checkout success path; it requires the
* readiness lane, mobile browser matrix, E2E_TENANT_DASHBOARD_PATH, and
* E2E_TENANT_SUBSCRIPTION_SUCCESS_PATH instead of letting that smoke skip.
*
* Otherwise runs the default Playwright suite under `tests/e2e/`. Set
* E2E_BROWSER_MATRIX=1 to add mobile Chromium/WebKit projects from
* playwright.config.ts.
*/
import { spawnSync } from "node:child_process";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
export const DEFAULT_TARGET_URL = "http://localhost:3000";
export function resolveTargetUrl({ env = process.env } = {}) {
const configuredUrl = env.E2E_BASE_URL?.trim();
return configuredUrl || DEFAULT_TARGET_URL;
}
export function hasPlaywrightRuntime({ spawn = spawnSync } = {}) {
const result = spawn("npx", ["--no-install", "playwright", "--version"], {
stdio: "ignore",
timeout: 30_000,
});
return result.status === 0;
}
export function isUrlReachable(url, { spawn = spawnSync, allowServerErrors = true } = {}) {
const result = spawn(
"curl",
["-fsS", "--max-time", "5", "-o", "/dev/null", "-w", "%{http_code}", url],
{ encoding: "utf8", timeout: 10_000 },
);
if (result.status !== 0) return false;
const code = parseInt((result.stdout || "").trim(), 10);
// In local SKIP-safe mode, any HTTP response means the app is reachable and
// Playwright can decide which pages should pass or skip. Strict readiness
// lanes fail early on 5xx so a broken staging target cannot look like a pass.
if (!Number.isFinite(code) || code <= 0) return false;
return allowServerErrors || code < 500;
}
function defaultStdout(message) {
process.stdout.write(`[e2e-a11y] ${message}\n`);
}
function requiredFailure({ stdout, reason, message }) {
stdout(`FAIL: ${message}`);
return { status: "fail", reason, exitCode: 1 };
}
export function hasRequiredAuthEnv({ env = process.env } = {}) {
return Boolean(env.E2E_TENANT_EMAIL && env.E2E_TENANT_PASSWORD);
}
export function missingReadinessEnv({ env = process.env } = {}) {
const required = [
"E2E_TENANT_EMAIL",
"E2E_TENANT_PASSWORD",
"E2E_REGISTRATION_EXPECTS_EMAIL_VERIFICATION",
"E2E_TENANT_DASHBOARD_PATH",
"E2E_TENANT_SETTINGS_ACCOUNT_PATH",
"E2E_TENANT_SETTINGS_PROFILE_PATH",
"E2E_TENANT_SUBSCRIPTION_PATH",
"E2E_TENANT_MEMBERS_PATH",
"E2E_TENANT_ROLES_AND_PERMISSIONS_PATH",
"E2E_TENANT_NOTIFICATIONS_PATH",
"E2E_TENANT_LOGS_PATH",
"E2E_TENANT_LOG_EVENTS_PATH",
"E2E_TENANT_AUDIT_EXPORT_PATH",
"E2E_TENANT_PRIVACY_PATH",
"E2E_TENANT_CHATBOT_PATH",
"E2E_TENANT_AI_USAGE_PATH",
"E2E_TENANT_API_PATH",
"E2E_TENANT_API_KEYS_PATH",
"E2E_TENANT_API_DOCS_PATH",
"E2E_TENANT_API_LOGS_PATH",
"E2E_TENANT_POLICIES_PATH",
"E2E_TENANT_PORTAL_ANALYTICS_PATH",
"E2E_TENANT_PORTAL_SETTINGS_PATH",
"E2E_ADMIN_EMAIL_SEQUENCES_PATH",
"E2E_PLUGIN_SETTINGS_PATH",
"E2E_PLUGIN_READONLY_EMAIL",
"E2E_PLUGIN_READONLY_PASSWORD",
"E2E_WORKFLOW_ENGINE_PATH",
"E2E_WORKFLOW_BUILDER_PATH",
"E2E_WORKFLOW_RUN_API_PATH",
"E2E_WORKFLOW_RUN_MANUAL_PATH",
"E2E_WORKFLOW_WEBHOOK_BUILDER_PATH",
"E2E_WORKFLOW_WEBHOOK_RUN_MANUAL_PATH",
"E2E_WORKFLOW_WAITING_BUILDER_PATH",
"E2E_WORKFLOW_WAITING_RUN_MANUAL_PATH",
"E2E_WORKFLOW_WAITING_RUN_API_PATH",
"E2E_WORKFLOW_WAITING_EXECUTIONS_PATH",
"E2E_WORKFLOW_WAITING_EXECUTION_ID",
"PLUGIN_BINARY_BASE_PATH",
];
const missing = required.filter((key) => !env[key]?.trim());
const registrationVerification = env.E2E_REGISTRATION_EXPECTS_EMAIL_VERIFICATION;
if (registrationVerification && registrationVerification !== "0" && registrationVerification !== "1") {
missing.push("E2E_REGISTRATION_EXPECTS_EMAIL_VERIFICATION=0|1");
}
return missing;
}
export function missingReadinessModeEnv({ env = process.env } = {}) {
const requiredModes = {
E2E_A11Y_STRICT: "1",
E2E_BROWSER_MATRIX: "1",
E2E_ALLOW_WORKFLOW_RUN: "1",
};
return Object.entries(requiredModes)
.filter(([key, expected]) => env[key] !== expected)
.map(([key, expected]) => `${key}=${expected}`);
}
export function missingReadinessMutatingOptInEnv({ env = process.env } = {}) {
const requiredOptIns = {
E2E_ALLOW_REGISTRATION_CREATE: "1",
E2E_ALLOW_ACCOUNT_CREATE: "1",
E2E_ALLOW_NO_TENANT_ACCOUNT_CREATE: "1",
E2E_ALLOW_PLUGIN_LIFECYCLE_MUTATION: "1",
E2E_ALLOW_PLUGIN_SETTINGS_WRITE: "1",
};
return Object.entries(requiredOptIns)
.filter(([key, expected]) => env[key] !== expected)
.map(([key, expected]) => `${key}=${expected}`);
}
export function missingNoTenantAccountCreateEnv({ env = process.env } = {}) {
if (env.E2E_ALLOW_NO_TENANT_ACCOUNT_CREATE !== "1") return [];
return ["E2E_NO_TENANT_EMAIL", "E2E_NO_TENANT_PASSWORD"].filter((key) => !env[key]?.trim());
}
export function missingSubscriptionSuccessReadinessEnv({ env = process.env } = {}) {
if (env.E2E_REQUIRE_SUBSCRIPTION_SUCCESS !== "1") return [];
const missing = ["E2E_TENANT_SUBSCRIPTION_SUCCESS_PATH", "E2E_TENANT_DASHBOARD_PATH"].filter((key) => !env[key]?.trim());
if (env.REQUIRE_E2E_READINESS !== "1") {
missing.push("REQUIRE_E2E_READINESS=1");
}
if (env.E2E_BROWSER_MATRIX !== "1") {
missing.push("E2E_BROWSER_MATRIX=1");
}
return missing;
}
export function missingWorkflowWebhookRunReadinessEnv({ env = process.env } = {}) {
if (env.E2E_ALLOW_WORKFLOW_WEBHOOK_RUN !== "1") return [];
return [
"E2E_TENANT_EMAIL",
"E2E_TENANT_PASSWORD",
"E2E_WORKFLOW_WEBHOOK_BUILDER_PATH",
"E2E_WORKFLOW_WEBHOOK_RUN_MANUAL_PATH",
"E2E_WORKFLOW_WEBHOOK_TARGET_URL",
].filter((key) => !env[key]?.trim());
}
export function missingWorkflowOutboxDrainReadinessEnv({ env = process.env } = {}) {
if (env.E2E_REQUIRE_WORKFLOW_OUTBOX_DRAIN !== "1") return [];
const missing = ["E2E_TENANT_EMAIL", "E2E_TENANT_PASSWORD", "E2E_WORKFLOW_RUN_MANUAL_PATH"].filter((key) => !env[key]?.trim());
const expectedStatus = env.E2E_WORKFLOW_OUTBOX_DRAIN_EXPECTED_STATUS?.trim().toLowerCase();
if (expectedStatus && expectedStatus !== "success" && expectedStatus !== "error") {
missing.push("E2E_WORKFLOW_OUTBOX_DRAIN_EXPECTED_STATUS=success|error");
}
return missing;
}
export function runPlaywrightA11y({
env = process.env,
spawn = spawnSync,
stdout = defaultStdout,
} = {}) {
const targetUrl = resolveTargetUrl({ env });
const requireE2eA11y = env.REQUIRE_E2E_A11Y === "1";
const requireAuthE2e = env.REQUIRE_AUTH_E2E === "1";
const requireReadinessE2e = env.REQUIRE_E2E_READINESS === "1";
const requireMutatingReadinessE2e = env.REQUIRE_E2E_MUTATING_READINESS === "1";
if (env.SKIP_E2E_A11Y === "1") {
const requiredLanes = [
["REQUIRE_E2E_A11Y", requireE2eA11y],
["REQUIRE_AUTH_E2E", requireAuthE2e],
["REQUIRE_E2E_READINESS", requireReadinessE2e],
["REQUIRE_E2E_MUTATING_READINESS", requireMutatingReadinessE2e],
["E2E_REQUIRE_SUBSCRIPTION_SUCCESS", env.E2E_REQUIRE_SUBSCRIPTION_SUCCESS === "1"],
]
.filter(([, enabled]) => enabled)
.map(([key]) => `${key}=1`);
if (requiredLanes.length > 0) {
return requiredFailure({
stdout,
reason: "explicit-skip-in-required-lane",
message: `SKIP_E2E_A11Y=1 cannot be used with required E2E lanes: ${requiredLanes.join(", ")}.`,
});
}
stdout("SKIP_E2E_A11Y=1 set — skipping.");
return { status: "skip", reason: "explicit-skip", exitCode: 0 };
}
const missingWorkflowWebhookRun = missingWorkflowWebhookRunReadinessEnv({ env });
if (missingWorkflowWebhookRun.length > 0) {
return requiredFailure({
stdout,
reason: "workflow-webhook-run-readiness-env-missing",
message: `E2E workflow webhook run readiness env missing: ${missingWorkflowWebhookRun.join(", ")} while E2E_ALLOW_WORKFLOW_WEBHOOK_RUN=1.`,
});
}
const missingWorkflowOutboxDrain = missingWorkflowOutboxDrainReadinessEnv({ env });
if (missingWorkflowOutboxDrain.length > 0) {
return requiredFailure({
stdout,
reason: "workflow-outbox-drain-readiness-env-missing",
message: `E2E workflow outbox-drain readiness env missing: ${missingWorkflowOutboxDrain.join(", ")} while E2E_REQUIRE_WORKFLOW_OUTBOX_DRAIN=1.`,
});
}
const missingSubscriptionSuccess = missingSubscriptionSuccessReadinessEnv({ env });
if (missingSubscriptionSuccess.length > 0) {
return requiredFailure({
stdout,
reason: "subscription-success-readiness-env-missing",
message: `E2E subscription success readiness env missing: ${missingSubscriptionSuccess.join(", ")} while E2E_REQUIRE_SUBSCRIPTION_SUCCESS=1.`,
});
}
const missingNoTenantAccountCreate = missingNoTenantAccountCreateEnv({ env });
if (missingNoTenantAccountCreate.length > 0) {
return requiredFailure({
stdout,
reason: "no-tenant-account-create-env-missing",
message: `E2E no-tenant account creation env missing: ${missingNoTenantAccountCreate.join(", ")} while E2E_ALLOW_NO_TENANT_ACCOUNT_CREATE=1.`,
});
}
if (!hasPlaywrightRuntime({ spawn })) {
if (requireE2eA11y) {
return requiredFailure({
stdout,
reason: "runtime-missing",
message: "Playwright runtime not installed while REQUIRE_E2E_A11Y=1.",
});
}
stdout("SKIP: Playwright runtime not installed (run `pnpm install` and `pnpm exec playwright install chromium`).");
return { status: "skip", reason: "runtime-missing", exitCode: 0 };
}
if (!isUrlReachable(targetUrl, { spawn, allowServerErrors: !requireE2eA11y })) {
if (requireE2eA11y) {
return requiredFailure({
stdout,
reason: "target-unreachable",
message: `Target URL ${targetUrl} not reachable while REQUIRE_E2E_A11Y=1.`,
});
}
stdout(`SKIP: Target URL ${targetUrl} not reachable.`);
stdout("Hint: start the app with `pnpm dev` or set E2E_BASE_URL=<staging-url>.");
return { status: "skip", reason: "target-unreachable", exitCode: 0 };
}
if (requireAuthE2e && !hasRequiredAuthEnv({ env })) {
return requiredFailure({
stdout,
reason: "auth-env-missing",
message: "E2E_TENANT_EMAIL / E2E_TENANT_PASSWORD not set while REQUIRE_AUTH_E2E=1.",
});
}
if (requireReadinessE2e) {
const missing = missingReadinessEnv({ env });
if (missing.length > 0) {
return requiredFailure({
stdout,
reason: "readiness-env-missing",
message: `E2E readiness env missing: ${missing.join(", ")}.`,
});
}
const missingModes = missingReadinessModeEnv({ env });
if (missingModes.length > 0) {
return requiredFailure({
stdout,
reason: "readiness-mode-missing",
message: `E2E readiness mode missing: ${missingModes.join(", ")}.`,
});
}
const missingMutatingOptIns = requireMutatingReadinessE2e ? missingReadinessMutatingOptInEnv({ env }) : [];
if (missingMutatingOptIns.length > 0) {
return requiredFailure({
stdout,
reason: "readiness-mutating-opt-in-missing",
message: `E2E readiness mutating opt-in missing: ${missingMutatingOptIns.join(", ")}.`,
});
}
}
stdout(`Target ${targetUrl} reachable — running Playwright a11y suite.`);
const result = spawn(
"pnpm",
["exec", "playwright", "test", "--config=playwright.config.ts"],
{ stdio: "inherit" },
);
if (result.error) {
return { status: "fail", reason: "spawn-error", exitCode: 1 };
}
if (typeof result.status === "number" && result.status !== 0) {
return { status: "fail", reason: "non-zero-exit", exitCode: result.status };
}
return { status: "pass", reason: "ok", exitCode: 0 };
}
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
if (isCli) {
const outcome = runPlaywrightA11y();
process.exit(outcome.exitCode);
}