994 lines
41 KiB
JavaScript
994 lines
41 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import { describe, it } from "vitest";
|
|
|
|
import {
|
|
hasRequiredAuthEnv,
|
|
isUrlReachable,
|
|
missingReadinessEnv,
|
|
missingReadinessModeEnv,
|
|
missingReadinessMutatingOptInEnv,
|
|
missingNoTenantAccountCreateEnv,
|
|
missingSubscriptionSuccessReadinessEnv,
|
|
missingWorkflowOutboxDrainReadinessEnv,
|
|
missingWorkflowWebhookRunReadinessEnv,
|
|
resolveTargetUrl,
|
|
runPlaywrightA11y,
|
|
} from "./run-playwright-a11y.mjs";
|
|
|
|
function recordingSpawn(handlers) {
|
|
const calls = [];
|
|
function spawn(command, args, options) {
|
|
calls.push({ command, args, options });
|
|
const handler = handlers[command];
|
|
if (!handler) {
|
|
return { status: 1, stdout: "", stderr: "", error: new Error(`unexpected spawn for ${command}`) };
|
|
}
|
|
return handler({ args, options });
|
|
}
|
|
spawn.calls = calls;
|
|
return spawn;
|
|
}
|
|
|
|
function captureLines() {
|
|
const lines = [];
|
|
function write(line) {
|
|
lines.push(line);
|
|
}
|
|
write.lines = lines;
|
|
return write;
|
|
}
|
|
|
|
function completeReadinessEnv(overrides = {}) {
|
|
return {
|
|
E2E_BASE_URL: "https://staging.example.test",
|
|
E2E_TENANT_EMAIL: "user@example.test",
|
|
E2E_TENANT_PASSWORD: "secret",
|
|
E2E_NO_TENANT_EMAIL: "no-tenant@example.test",
|
|
E2E_NO_TENANT_PASSWORD: "no-tenant-secret",
|
|
E2E_REGISTRATION_EXPECTS_EMAIL_VERIFICATION: "0",
|
|
E2E_TENANT_DASHBOARD_PATH: "/app/acme/dashboard",
|
|
E2E_TENANT_SETTINGS_ACCOUNT_PATH: "/app/acme/settings/account",
|
|
E2E_TENANT_SETTINGS_PROFILE_PATH: "/app/acme/settings/profile",
|
|
E2E_TENANT_SUBSCRIPTION_PATH: "/app/acme/settings/subscription",
|
|
E2E_TENANT_MEMBERS_PATH: "/app/acme/settings/members",
|
|
E2E_TENANT_ROLES_AND_PERMISSIONS_PATH: "/app/acme/settings/roles-and-permissions",
|
|
E2E_TENANT_NOTIFICATIONS_PATH: "/app/acme/settings/notifications",
|
|
E2E_TENANT_LOGS_PATH: "/app/acme/settings/logs",
|
|
E2E_TENANT_LOG_EVENTS_PATH: "/app/acme/settings/logs/events",
|
|
E2E_TENANT_AUDIT_EXPORT_PATH: "/app/acme/settings/audit-export",
|
|
E2E_TENANT_PRIVACY_PATH: "/app/acme/settings/privacy",
|
|
E2E_TENANT_CHATBOT_PATH: "/app/acme/settings/chatbot",
|
|
E2E_TENANT_AI_USAGE_PATH: "/app/acme/settings/ai-usage",
|
|
E2E_TENANT_API_PATH: "/app/acme/settings/api",
|
|
E2E_TENANT_API_KEYS_PATH: "/app/acme/settings/api/keys",
|
|
E2E_TENANT_API_DOCS_PATH: "/app/acme/settings/api/docs",
|
|
E2E_TENANT_API_LOGS_PATH: "/app/acme/settings/api/logs",
|
|
E2E_TENANT_POLICIES_PATH: "/app/acme/settings/policies",
|
|
E2E_TENANT_PORTAL_ANALYTICS_PATH: "/app/acme/portals/readiness/analytics",
|
|
E2E_TENANT_PORTAL_SETTINGS_PATH: "/app/acme/portals/readiness/settings",
|
|
E2E_ADMIN_EMAIL_SEQUENCES_PATH: "/admin/email-sequences",
|
|
E2E_PLUGIN_SETTINGS_PATH: "/app/acme/settings/plugins/plugin-1",
|
|
E2E_PLUGIN_READONLY_EMAIL: "plugin-readonly@example.test",
|
|
E2E_PLUGIN_READONLY_PASSWORD: "readonly-secret",
|
|
E2E_WORKFLOW_ENGINE_PATH: "/app/acme/workflows",
|
|
E2E_WORKFLOW_BUILDER_PATH: "/app/acme/workflows/wf-1/builder",
|
|
E2E_WORKFLOW_RUN_API_PATH: "/app/acme/workflows/wf-1/run/api",
|
|
E2E_WORKFLOW_RUN_MANUAL_PATH: "/app/acme/workflows/wf-1/run",
|
|
E2E_WORKFLOW_WEBHOOK_BUILDER_PATH: "/app/acme/workflows/wf-webhook/builder",
|
|
E2E_WORKFLOW_WEBHOOK_RUN_MANUAL_PATH: "/app/acme/workflows/wf-webhook/run",
|
|
E2E_WORKFLOW_WAITING_BUILDER_PATH: "/app/acme/workflows/wf-waiting/builder",
|
|
E2E_WORKFLOW_WAITING_RUN_MANUAL_PATH: "/app/acme/workflows/wf-waiting/run",
|
|
E2E_WORKFLOW_WAITING_RUN_API_PATH: "/app/acme/workflows/wf-waiting/run/api",
|
|
E2E_WORKFLOW_WAITING_EXECUTIONS_PATH: "/app/acme/workflows/wf-waiting/executions",
|
|
E2E_WORKFLOW_WAITING_EXECUTION_ID: "wfe-waiting-1",
|
|
PLUGIN_BINARY_BASE_PATH: "/tmp/e2e-plugins",
|
|
E2E_A11Y_STRICT: "1",
|
|
E2E_BROWSER_MATRIX: "1",
|
|
E2E_ALLOW_WORKFLOW_RUN: "1",
|
|
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",
|
|
REQUIRE_E2E_READINESS: "1",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("run-playwright-a11y", () => {
|
|
it("uses E2E_BASE_URL when provided", () => {
|
|
assert.equal(resolveTargetUrl({ env: { E2E_BASE_URL: "https://staging.example.test" } }), "https://staging.example.test");
|
|
});
|
|
|
|
it("falls back to localhost when E2E_BASE_URL is missing", () => {
|
|
assert.equal(resolveTargetUrl({ env: {} }), "http://localhost:3000");
|
|
});
|
|
|
|
it("falls back to localhost when E2E_BASE_URL is blank", () => {
|
|
assert.equal(resolveTargetUrl({ env: { E2E_BASE_URL: "" } }), "http://localhost:3000");
|
|
assert.equal(resolveTargetUrl({ env: { E2E_BASE_URL: " " } }), "http://localhost:3000");
|
|
});
|
|
|
|
it("trims E2E_BASE_URL before probing the target", () => {
|
|
assert.equal(resolveTargetUrl({ env: { E2E_BASE_URL: " https://staging.example.test " } }), "https://staging.example.test");
|
|
});
|
|
|
|
it("SKIPs with exit 0 when SKIP_E2E_A11Y=1 is set", () => {
|
|
const spawn = recordingSpawn({});
|
|
const stdout = captureLines();
|
|
|
|
const outcome = runPlaywrightA11y({ env: { SKIP_E2E_A11Y: "1" }, spawn, stdout });
|
|
|
|
assert.deepEqual(outcome, { status: "skip", reason: "explicit-skip", exitCode: 0 });
|
|
assert.deepEqual(stdout.lines, ["SKIP_E2E_A11Y=1 set — skipping."]);
|
|
assert.equal(spawn.calls.length, 0);
|
|
});
|
|
|
|
it("fails instead of honoring SKIP_E2E_A11Y in required readiness lanes", () => {
|
|
const spawn = recordingSpawn({});
|
|
const stdout = captureLines();
|
|
|
|
const outcome = runPlaywrightA11y({
|
|
env: {
|
|
SKIP_E2E_A11Y: "1",
|
|
REQUIRE_E2E_A11Y: "1",
|
|
REQUIRE_AUTH_E2E: "1",
|
|
REQUIRE_E2E_READINESS: "1",
|
|
E2E_REQUIRE_SUBSCRIPTION_SUCCESS: "1",
|
|
},
|
|
spawn,
|
|
stdout,
|
|
});
|
|
|
|
assert.deepEqual(outcome, { status: "fail", reason: "explicit-skip-in-required-lane", exitCode: 1 });
|
|
assert.deepEqual(stdout.lines, [
|
|
"FAIL: SKIP_E2E_A11Y=1 cannot be used with required E2E lanes: REQUIRE_E2E_A11Y=1, REQUIRE_AUTH_E2E=1, REQUIRE_E2E_READINESS=1, E2E_REQUIRE_SUBSCRIPTION_SUCCESS=1.",
|
|
]);
|
|
assert.equal(spawn.calls.length, 0);
|
|
});
|
|
|
|
it("SKIPs with exit 0 when the Playwright runtime is missing", () => {
|
|
const spawn = recordingSpawn({
|
|
npx: () => ({ status: 1, stdout: "", stderr: "", error: new Error("ENOENT") }),
|
|
});
|
|
const stdout = captureLines();
|
|
|
|
const outcome = runPlaywrightA11y({ env: {}, spawn, stdout });
|
|
|
|
assert.deepEqual(outcome, { status: "skip", reason: "runtime-missing", exitCode: 0 });
|
|
assert.match(stdout.lines[0], /^SKIP: Playwright runtime not installed/);
|
|
assert.equal(spawn.calls.filter((call) => call.command === "curl").length, 0);
|
|
});
|
|
|
|
it("fails instead of skipping missing runtime when REQUIRE_E2E_A11Y=1", () => {
|
|
const spawn = recordingSpawn({
|
|
npx: () => ({ status: 1, stdout: "", stderr: "", error: new Error("ENOENT") }),
|
|
});
|
|
const stdout = captureLines();
|
|
|
|
const outcome = runPlaywrightA11y({ env: { REQUIRE_E2E_A11Y: "1" }, spawn, stdout });
|
|
|
|
assert.deepEqual(outcome, { status: "fail", reason: "runtime-missing", exitCode: 1 });
|
|
assert.deepEqual(stdout.lines, ["FAIL: Playwright runtime not installed while REQUIRE_E2E_A11Y=1."]);
|
|
});
|
|
|
|
it("SKIPs with exit 0 when the target URL is unreachable", () => {
|
|
const spawn = recordingSpawn({
|
|
npx: ({ args }) => {
|
|
assert.deepEqual(args, ["--no-install", "playwright", "--version"]);
|
|
return { status: 0, stdout: "Version 1.59.1", stderr: "" };
|
|
},
|
|
curl: () => ({ status: 7, stdout: "", stderr: "Connection refused" }),
|
|
});
|
|
const stdout = captureLines();
|
|
|
|
const outcome = runPlaywrightA11y({
|
|
env: { E2E_BASE_URL: "http://offline.example.test" },
|
|
spawn,
|
|
stdout,
|
|
});
|
|
|
|
assert.deepEqual(outcome, { status: "skip", reason: "target-unreachable", exitCode: 0 });
|
|
assert.ok(stdout.lines.some((line) => line === "SKIP: Target URL http://offline.example.test not reachable."));
|
|
assert.equal(spawn.calls.filter((call) => call.command === "npx" && call.args[0] === "playwright").length, 0);
|
|
});
|
|
|
|
it("fails instead of skipping unreachable targets when REQUIRE_E2E_A11Y=1", () => {
|
|
const spawn = recordingSpawn({
|
|
npx: ({ args }) => {
|
|
assert.deepEqual(args, ["--no-install", "playwright", "--version"]);
|
|
return { status: 0, stdout: "Version 1.59.1", stderr: "" };
|
|
},
|
|
curl: () => ({ status: 7, stdout: "", stderr: "Connection refused" }),
|
|
});
|
|
const stdout = captureLines();
|
|
|
|
const outcome = runPlaywrightA11y({
|
|
env: { E2E_BASE_URL: "http://offline.example.test", REQUIRE_E2E_A11Y: "1" },
|
|
spawn,
|
|
stdout,
|
|
});
|
|
|
|
assert.deepEqual(outcome, { status: "fail", reason: "target-unreachable", exitCode: 1 });
|
|
assert.deepEqual(stdout.lines, ["FAIL: Target URL http://offline.example.test not reachable while REQUIRE_E2E_A11Y=1."]);
|
|
});
|
|
|
|
it("fails before running Playwright when authenticated E2E credentials are required but missing", () => {
|
|
const spawn = recordingSpawn({
|
|
npx: ({ args }) => {
|
|
if (args[0] === "--no-install") {
|
|
return { status: 0, stdout: "Version 1.59.1", stderr: "" };
|
|
}
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
},
|
|
pnpm: () => ({ status: 0, stdout: "", stderr: "" }),
|
|
curl: () => ({ status: 0, stdout: "200", stderr: "" }),
|
|
});
|
|
const stdout = captureLines();
|
|
|
|
const outcome = runPlaywrightA11y({
|
|
env: { E2E_BASE_URL: "https://staging.example.test", REQUIRE_AUTH_E2E: "1" },
|
|
spawn,
|
|
stdout,
|
|
});
|
|
|
|
assert.deepEqual(outcome, { status: "fail", reason: "auth-env-missing", exitCode: 1 });
|
|
assert.deepEqual(stdout.lines, ["FAIL: E2E_TENANT_EMAIL / E2E_TENANT_PASSWORD not set while REQUIRE_AUTH_E2E=1."]);
|
|
assert.equal(spawn.calls.filter((call) => call.command === "npx" && call.args[0] === "playwright").length, 0);
|
|
});
|
|
|
|
it("detects authenticated E2E credentials", () => {
|
|
assert.equal(hasRequiredAuthEnv({ env: {} }), false);
|
|
assert.equal(hasRequiredAuthEnv({ env: { E2E_TENANT_EMAIL: "user@example.test" } }), false);
|
|
assert.equal(hasRequiredAuthEnv({ env: { E2E_TENANT_EMAIL: "user@example.test", E2E_TENANT_PASSWORD: "secret" } }), true);
|
|
});
|
|
|
|
it("fails before running Playwright when readiness E2E inputs are incomplete", () => {
|
|
const spawn = recordingSpawn({
|
|
npx: ({ args }) => {
|
|
if (args[0] === "--no-install") {
|
|
return { status: 0, stdout: "Version 1.59.1", stderr: "" };
|
|
}
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
},
|
|
pnpm: () => ({ status: 0, stdout: "", stderr: "" }),
|
|
curl: () => ({ status: 0, stdout: "200", stderr: "" }),
|
|
});
|
|
const stdout = captureLines();
|
|
|
|
const outcome = runPlaywrightA11y({
|
|
env: {
|
|
E2E_BASE_URL: "https://staging.example.test",
|
|
E2E_TENANT_EMAIL: "user@example.test",
|
|
E2E_TENANT_PASSWORD: "secret",
|
|
REQUIRE_E2E_READINESS: "1",
|
|
},
|
|
spawn,
|
|
stdout,
|
|
});
|
|
|
|
assert.deepEqual(outcome, { status: "fail", reason: "readiness-env-missing", exitCode: 1 });
|
|
assert.match(stdout.lines[0], /E2E readiness env missing:/);
|
|
assert.match(stdout.lines[0], /E2E_REGISTRATION_EXPECTS_EMAIL_VERIFICATION/);
|
|
assert.match(stdout.lines[0], /E2E_TENANT_DASHBOARD_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_TENANT_SETTINGS_ACCOUNT_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_TENANT_SETTINGS_PROFILE_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_TENANT_SUBSCRIPTION_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_TENANT_MEMBERS_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_TENANT_ROLES_AND_PERMISSIONS_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_TENANT_NOTIFICATIONS_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_TENANT_LOGS_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_TENANT_LOG_EVENTS_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_TENANT_AUDIT_EXPORT_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_TENANT_PRIVACY_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_TENANT_CHATBOT_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_TENANT_AI_USAGE_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_TENANT_API_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_TENANT_API_KEYS_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_TENANT_API_DOCS_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_TENANT_API_LOGS_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_TENANT_POLICIES_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_TENANT_PORTAL_ANALYTICS_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_TENANT_PORTAL_SETTINGS_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_PLUGIN_SETTINGS_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_PLUGIN_READONLY_EMAIL/);
|
|
assert.match(stdout.lines[0], /E2E_PLUGIN_READONLY_PASSWORD/);
|
|
assert.match(stdout.lines[0], /E2E_WORKFLOW_ENGINE_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_WORKFLOW_BUILDER_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_WORKFLOW_RUN_API_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_WORKFLOW_RUN_MANUAL_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_WORKFLOW_WEBHOOK_BUILDER_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_WORKFLOW_WEBHOOK_RUN_MANUAL_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_WORKFLOW_WAITING_BUILDER_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_WORKFLOW_WAITING_RUN_MANUAL_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_WORKFLOW_WAITING_RUN_API_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_WORKFLOW_WAITING_EXECUTIONS_PATH/);
|
|
assert.match(stdout.lines[0], /E2E_WORKFLOW_WAITING_EXECUTION_ID/);
|
|
assert.match(stdout.lines[0], /PLUGIN_BINARY_BASE_PATH/);
|
|
assert.doesNotMatch(stdout.lines[0], /E2E_TENANT_SUBSCRIPTION_SUCCESS_PATH/);
|
|
assert.doesNotMatch(stdout.lines[0], /E2E_ALLOW_WORKFLOW_RUN=1/);
|
|
assert.doesNotMatch(stdout.lines[0], /E2E_ALLOW_REGISTRATION_CREATE=1/);
|
|
assert.equal(spawn.calls.filter((call) => call.command === "npx" && call.args[0] === "playwright").length, 0);
|
|
});
|
|
|
|
it("accepts the complete readiness E2E env contract", () => {
|
|
assert.deepEqual(
|
|
missingReadinessEnv({
|
|
env: {
|
|
E2E_TENANT_EMAIL: "user@example.test",
|
|
E2E_TENANT_PASSWORD: "secret",
|
|
E2E_REGISTRATION_EXPECTS_EMAIL_VERIFICATION: "0",
|
|
E2E_TENANT_DASHBOARD_PATH: "/app/acme/dashboard",
|
|
E2E_TENANT_SETTINGS_ACCOUNT_PATH: "/app/acme/settings/account",
|
|
E2E_TENANT_SETTINGS_PROFILE_PATH: "/app/acme/settings/profile",
|
|
E2E_TENANT_SUBSCRIPTION_PATH: "/app/acme/settings/subscription",
|
|
E2E_TENANT_MEMBERS_PATH: "/app/acme/settings/members",
|
|
E2E_TENANT_ROLES_AND_PERMISSIONS_PATH: "/app/acme/settings/roles-and-permissions",
|
|
E2E_TENANT_NOTIFICATIONS_PATH: "/app/acme/settings/notifications",
|
|
E2E_TENANT_LOGS_PATH: "/app/acme/settings/logs",
|
|
E2E_TENANT_LOG_EVENTS_PATH: "/app/acme/settings/logs/events",
|
|
E2E_TENANT_AUDIT_EXPORT_PATH: "/app/acme/settings/audit-export",
|
|
E2E_TENANT_PRIVACY_PATH: "/app/acme/settings/privacy",
|
|
E2E_TENANT_CHATBOT_PATH: "/app/acme/settings/chatbot",
|
|
E2E_TENANT_AI_USAGE_PATH: "/app/acme/settings/ai-usage",
|
|
E2E_TENANT_API_PATH: "/app/acme/settings/api",
|
|
E2E_TENANT_API_KEYS_PATH: "/app/acme/settings/api/keys",
|
|
E2E_TENANT_API_DOCS_PATH: "/app/acme/settings/api/docs",
|
|
E2E_TENANT_API_LOGS_PATH: "/app/acme/settings/api/logs",
|
|
E2E_TENANT_POLICIES_PATH: "/app/acme/settings/policies",
|
|
E2E_TENANT_PORTAL_ANALYTICS_PATH: "/app/acme/portals/readiness/analytics",
|
|
E2E_TENANT_PORTAL_SETTINGS_PATH: "/app/acme/portals/readiness/settings",
|
|
E2E_ADMIN_EMAIL_SEQUENCES_PATH: "/admin/email-sequences",
|
|
E2E_PLUGIN_SETTINGS_PATH: "/app/acme/settings/plugins/plugin-1",
|
|
E2E_PLUGIN_READONLY_EMAIL: "plugin-readonly@example.test",
|
|
E2E_PLUGIN_READONLY_PASSWORD: "readonly-secret",
|
|
E2E_WORKFLOW_ENGINE_PATH: "/app/acme/workflows",
|
|
E2E_WORKFLOW_BUILDER_PATH: "/app/acme/workflows/wf-1/builder",
|
|
E2E_WORKFLOW_RUN_API_PATH: "/app/acme/workflows/wf-1/run/api",
|
|
E2E_WORKFLOW_RUN_MANUAL_PATH: "/app/acme/workflows/wf-1/run",
|
|
E2E_WORKFLOW_WEBHOOK_BUILDER_PATH: "/app/acme/workflows/wf-webhook/builder",
|
|
E2E_WORKFLOW_WEBHOOK_RUN_MANUAL_PATH: "/app/acme/workflows/wf-webhook/run",
|
|
E2E_WORKFLOW_WAITING_BUILDER_PATH: "/app/acme/workflows/wf-waiting/builder",
|
|
E2E_WORKFLOW_WAITING_RUN_MANUAL_PATH: "/app/acme/workflows/wf-waiting/run",
|
|
E2E_WORKFLOW_WAITING_RUN_API_PATH: "/app/acme/workflows/wf-waiting/run/api",
|
|
E2E_WORKFLOW_WAITING_EXECUTIONS_PATH: "/app/acme/workflows/wf-waiting/executions",
|
|
E2E_WORKFLOW_WAITING_EXECUTION_ID: "wfe-waiting-1",
|
|
PLUGIN_BINARY_BASE_PATH: "/tmp/e2e-plugins",
|
|
},
|
|
}),
|
|
[]
|
|
);
|
|
});
|
|
|
|
it("rejects whitespace-only readiness values before Playwright starts", () => {
|
|
assert.deepEqual(
|
|
missingReadinessEnv({
|
|
env: completeReadinessEnv({
|
|
E2E_TENANT_DASHBOARD_PATH: " ",
|
|
E2E_PLUGIN_SETTINGS_PATH: "\n\t",
|
|
E2E_WORKFLOW_RUN_MANUAL_PATH: "",
|
|
}),
|
|
}),
|
|
["E2E_TENANT_DASHBOARD_PATH", "E2E_PLUGIN_SETTINGS_PATH", "E2E_WORKFLOW_RUN_MANUAL_PATH"]
|
|
);
|
|
});
|
|
|
|
it("keeps subscription success optional for normal readiness lanes", () => {
|
|
const env = completeReadinessEnv();
|
|
|
|
assert.deepEqual(missingReadinessEnv({ env }), []);
|
|
assert.deepEqual(missingSubscriptionSuccessReadinessEnv({ env }), []);
|
|
});
|
|
|
|
it("requires readiness and mobile dashboard follow-through env when subscription success strict mode is enabled", () => {
|
|
assert.deepEqual(
|
|
missingSubscriptionSuccessReadinessEnv({
|
|
env: { E2E_REQUIRE_SUBSCRIPTION_SUCCESS: "1" },
|
|
}),
|
|
["E2E_TENANT_SUBSCRIPTION_SUCCESS_PATH", "E2E_TENANT_DASHBOARD_PATH", "REQUIRE_E2E_READINESS=1", "E2E_BROWSER_MATRIX=1"]
|
|
);
|
|
assert.deepEqual(
|
|
missingSubscriptionSuccessReadinessEnv({
|
|
env: completeReadinessEnv({
|
|
E2E_REQUIRE_SUBSCRIPTION_SUCCESS: "1",
|
|
E2E_TENANT_SUBSCRIPTION_SUCCESS_PATH: "/subscribe/acme/cs_test_123/success",
|
|
}),
|
|
}),
|
|
[]
|
|
);
|
|
});
|
|
|
|
it("does not allow subscription success strict mode outside the readiness mobile matrix", () => {
|
|
assert.deepEqual(
|
|
missingSubscriptionSuccessReadinessEnv({
|
|
env: {
|
|
E2E_REQUIRE_SUBSCRIPTION_SUCCESS: "1",
|
|
E2E_TENANT_SUBSCRIPTION_SUCCESS_PATH: "/subscribe/acme/cs_test_123/success",
|
|
E2E_TENANT_DASHBOARD_PATH: "/app/acme/dashboard",
|
|
},
|
|
}),
|
|
["REQUIRE_E2E_READINESS=1", "E2E_BROWSER_MATRIX=1"]
|
|
);
|
|
});
|
|
|
|
it("requires strict a11y, real workflow execution, and the mobile browser matrix for readiness lanes", () => {
|
|
assert.deepEqual(
|
|
missingReadinessModeEnv({
|
|
env: {
|
|
E2E_A11Y_STRICT: "1",
|
|
},
|
|
}),
|
|
["E2E_BROWSER_MATRIX=1", "E2E_ALLOW_WORKFLOW_RUN=1"]
|
|
);
|
|
assert.deepEqual(
|
|
missingReadinessModeEnv({
|
|
env: {
|
|
E2E_A11Y_STRICT: "1",
|
|
E2E_BROWSER_MATRIX: "1",
|
|
E2E_ALLOW_WORKFLOW_RUN: "1",
|
|
},
|
|
}),
|
|
[]
|
|
);
|
|
});
|
|
|
|
it("requires core mutating opt-ins for readiness lanes", () => {
|
|
assert.deepEqual(
|
|
missingReadinessMutatingOptInEnv({
|
|
env: {
|
|
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",
|
|
]
|
|
);
|
|
assert.deepEqual(
|
|
missingReadinessMutatingOptInEnv({
|
|
env: {
|
|
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",
|
|
},
|
|
}),
|
|
[]
|
|
);
|
|
});
|
|
|
|
it("requires no-tenant credentials whenever no-tenant account creation is enabled", () => {
|
|
assert.deepEqual(missingNoTenantAccountCreateEnv({ env: {} }), []);
|
|
assert.deepEqual(
|
|
missingNoTenantAccountCreateEnv({
|
|
env: { E2E_ALLOW_NO_TENANT_ACCOUNT_CREATE: "1" },
|
|
}),
|
|
["E2E_NO_TENANT_EMAIL", "E2E_NO_TENANT_PASSWORD"]
|
|
);
|
|
assert.deepEqual(
|
|
missingNoTenantAccountCreateEnv({
|
|
env: {
|
|
E2E_ALLOW_NO_TENANT_ACCOUNT_CREATE: "1",
|
|
E2E_NO_TENANT_EMAIL: "no-tenant@example.test",
|
|
E2E_NO_TENANT_PASSWORD: "NoTenantPass123!",
|
|
},
|
|
}),
|
|
[]
|
|
);
|
|
});
|
|
|
|
it("fails before running Playwright when no-tenant account creation is enabled without credentials", () => {
|
|
const spawn = recordingSpawn({
|
|
npx: ({ args }) => {
|
|
if (args[0] === "--no-install") {
|
|
return { status: 0, stdout: "Version 1.59.1", stderr: "" };
|
|
}
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
},
|
|
pnpm: () => ({ status: 0, stdout: "", stderr: "" }),
|
|
curl: () => ({ status: 0, stdout: "200", stderr: "" }),
|
|
});
|
|
const stdout = captureLines();
|
|
|
|
const outcome = runPlaywrightA11y({
|
|
env: {
|
|
E2E_BASE_URL: "https://staging.example.test",
|
|
E2E_ALLOW_NO_TENANT_ACCOUNT_CREATE: "1",
|
|
},
|
|
spawn,
|
|
stdout,
|
|
});
|
|
|
|
assert.deepEqual(outcome, { status: "fail", reason: "no-tenant-account-create-env-missing", exitCode: 1 });
|
|
assert.deepEqual(stdout.lines, [
|
|
"FAIL: E2E no-tenant account creation env missing: E2E_NO_TENANT_EMAIL, E2E_NO_TENANT_PASSWORD while E2E_ALLOW_NO_TENANT_ACCOUNT_CREATE=1.",
|
|
]);
|
|
assert.equal(spawn.calls.length, 0);
|
|
});
|
|
|
|
it("fails before running Playwright when readiness mode is not strict/mobile", () => {
|
|
const spawn = recordingSpawn({
|
|
npx: ({ args }) => {
|
|
if (args[0] === "--no-install") {
|
|
return { status: 0, stdout: "Version 1.59.1", stderr: "" };
|
|
}
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
},
|
|
pnpm: () => ({ status: 0, stdout: "", stderr: "" }),
|
|
curl: () => ({ status: 0, stdout: "200", stderr: "" }),
|
|
});
|
|
const stdout = captureLines();
|
|
|
|
const outcome = runPlaywrightA11y({
|
|
env: {
|
|
E2E_BASE_URL: "https://staging.example.test",
|
|
E2E_TENANT_EMAIL: "user@example.test",
|
|
E2E_TENANT_PASSWORD: "secret",
|
|
E2E_REGISTRATION_EXPECTS_EMAIL_VERIFICATION: "0",
|
|
E2E_TENANT_DASHBOARD_PATH: "/app/acme/dashboard",
|
|
E2E_TENANT_SETTINGS_ACCOUNT_PATH: "/app/acme/settings/account",
|
|
E2E_TENANT_SETTINGS_PROFILE_PATH: "/app/acme/settings/profile",
|
|
E2E_TENANT_SUBSCRIPTION_PATH: "/app/acme/settings/subscription",
|
|
E2E_TENANT_MEMBERS_PATH: "/app/acme/settings/members",
|
|
E2E_TENANT_ROLES_AND_PERMISSIONS_PATH: "/app/acme/settings/roles-and-permissions",
|
|
E2E_TENANT_NOTIFICATIONS_PATH: "/app/acme/settings/notifications",
|
|
E2E_TENANT_LOGS_PATH: "/app/acme/settings/logs",
|
|
E2E_TENANT_LOG_EVENTS_PATH: "/app/acme/settings/logs/events",
|
|
E2E_TENANT_AUDIT_EXPORT_PATH: "/app/acme/settings/audit-export",
|
|
E2E_TENANT_PRIVACY_PATH: "/app/acme/settings/privacy",
|
|
E2E_TENANT_CHATBOT_PATH: "/app/acme/settings/chatbot",
|
|
E2E_TENANT_AI_USAGE_PATH: "/app/acme/settings/ai-usage",
|
|
E2E_TENANT_API_PATH: "/app/acme/settings/api",
|
|
E2E_TENANT_API_KEYS_PATH: "/app/acme/settings/api/keys",
|
|
E2E_TENANT_API_DOCS_PATH: "/app/acme/settings/api/docs",
|
|
E2E_TENANT_API_LOGS_PATH: "/app/acme/settings/api/logs",
|
|
E2E_TENANT_POLICIES_PATH: "/app/acme/settings/policies",
|
|
E2E_TENANT_PORTAL_ANALYTICS_PATH: "/app/acme/portals/readiness/analytics",
|
|
E2E_TENANT_PORTAL_SETTINGS_PATH: "/app/acme/portals/readiness/settings",
|
|
E2E_ADMIN_EMAIL_SEQUENCES_PATH: "/admin/email-sequences",
|
|
E2E_PLUGIN_SETTINGS_PATH: "/app/acme/settings/plugins/plugin-1",
|
|
E2E_PLUGIN_READONLY_EMAIL: "plugin-readonly@example.test",
|
|
E2E_PLUGIN_READONLY_PASSWORD: "readonly-secret",
|
|
E2E_WORKFLOW_ENGINE_PATH: "/app/acme/workflows",
|
|
E2E_WORKFLOW_BUILDER_PATH: "/app/acme/workflows/wf-1/builder",
|
|
E2E_WORKFLOW_RUN_API_PATH: "/app/acme/workflows/wf-1/run/api",
|
|
E2E_WORKFLOW_RUN_MANUAL_PATH: "/app/acme/workflows/wf-1/run",
|
|
E2E_WORKFLOW_WEBHOOK_BUILDER_PATH: "/app/acme/workflows/wf-webhook/builder",
|
|
E2E_WORKFLOW_WEBHOOK_RUN_MANUAL_PATH: "/app/acme/workflows/wf-webhook/run",
|
|
E2E_WORKFLOW_WAITING_BUILDER_PATH: "/app/acme/workflows/wf-waiting/builder",
|
|
E2E_WORKFLOW_WAITING_RUN_MANUAL_PATH: "/app/acme/workflows/wf-waiting/run",
|
|
E2E_WORKFLOW_WAITING_RUN_API_PATH: "/app/acme/workflows/wf-waiting/run/api",
|
|
E2E_WORKFLOW_WAITING_EXECUTIONS_PATH: "/app/acme/workflows/wf-waiting/executions",
|
|
E2E_WORKFLOW_WAITING_EXECUTION_ID: "wfe-waiting-1",
|
|
PLUGIN_BINARY_BASE_PATH: "/tmp/e2e-plugins",
|
|
REQUIRE_E2E_READINESS: "1",
|
|
},
|
|
spawn,
|
|
stdout,
|
|
});
|
|
|
|
assert.deepEqual(outcome, { status: "fail", reason: "readiness-mode-missing", exitCode: 1 });
|
|
assert.deepEqual(stdout.lines, ["FAIL: E2E readiness mode missing: E2E_A11Y_STRICT=1, E2E_BROWSER_MATRIX=1, E2E_ALLOW_WORKFLOW_RUN=1."]);
|
|
assert.equal(spawn.calls.filter((call) => call.command === "pnpm" && call.args[0] === "exec").length, 0);
|
|
});
|
|
|
|
it("does not require mutating opt-ins for normal non-mutating readiness lanes", () => {
|
|
const spawn = recordingSpawn({
|
|
npx: ({ args }) => {
|
|
if (args[0] === "--no-install") {
|
|
return { status: 0, stdout: "Version 1.59.1", stderr: "" };
|
|
}
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
},
|
|
pnpm: () => ({ status: 0, stdout: "", stderr: "" }),
|
|
curl: () => ({ status: 0, stdout: "200", stderr: "" }),
|
|
});
|
|
const stdout = captureLines();
|
|
|
|
const outcome = runPlaywrightA11y({
|
|
env: completeReadinessEnv({
|
|
E2E_ALLOW_REGISTRATION_CREATE: undefined,
|
|
E2E_ALLOW_ACCOUNT_CREATE: undefined,
|
|
E2E_ALLOW_NO_TENANT_ACCOUNT_CREATE: undefined,
|
|
E2E_ALLOW_PLUGIN_LIFECYCLE_MUTATION: undefined,
|
|
E2E_ALLOW_PLUGIN_SETTINGS_WRITE: undefined,
|
|
}),
|
|
spawn,
|
|
stdout,
|
|
});
|
|
|
|
assert.deepEqual(outcome, { status: "pass", reason: "ok", exitCode: 0 });
|
|
assert.equal(spawn.calls.filter((call) => call.command === "pnpm" && call.args[0] === "exec").length, 1);
|
|
});
|
|
|
|
it("fails before running Playwright when mutating readiness opt-ins are missing", () => {
|
|
const spawn = recordingSpawn({
|
|
npx: ({ args }) => {
|
|
if (args[0] === "--no-install") {
|
|
return { status: 0, stdout: "Version 1.59.1", stderr: "" };
|
|
}
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
},
|
|
pnpm: () => ({ status: 0, stdout: "", stderr: "" }),
|
|
curl: () => ({ status: 0, stdout: "200", stderr: "" }),
|
|
});
|
|
const stdout = captureLines();
|
|
|
|
const outcome = runPlaywrightA11y({
|
|
env: completeReadinessEnv({
|
|
E2E_ALLOW_REGISTRATION_CREATE: undefined,
|
|
E2E_ALLOW_ACCOUNT_CREATE: undefined,
|
|
E2E_ALLOW_NO_TENANT_ACCOUNT_CREATE: undefined,
|
|
E2E_ALLOW_PLUGIN_LIFECYCLE_MUTATION: undefined,
|
|
E2E_ALLOW_PLUGIN_SETTINGS_WRITE: undefined,
|
|
REQUIRE_E2E_MUTATING_READINESS: "1",
|
|
}),
|
|
spawn,
|
|
stdout,
|
|
});
|
|
|
|
assert.deepEqual(outcome, { status: "fail", reason: "readiness-mutating-opt-in-missing", exitCode: 1 });
|
|
assert.deepEqual(stdout.lines, [
|
|
"FAIL: E2E readiness mutating opt-in missing: 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.",
|
|
]);
|
|
assert.equal(spawn.calls.filter((call) => call.command === "pnpm" && call.args[0] === "exec").length, 0);
|
|
});
|
|
|
|
it("fails before running Playwright when subscription success strict mode is missing its path", () => {
|
|
const spawn = recordingSpawn({
|
|
npx: ({ args }) => {
|
|
if (args[0] === "--no-install") {
|
|
return { status: 0, stdout: "Version 1.59.1", stderr: "" };
|
|
}
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
},
|
|
pnpm: () => ({ status: 0, stdout: "", stderr: "" }),
|
|
curl: () => ({ status: 0, stdout: "200", stderr: "" }),
|
|
});
|
|
const stdout = captureLines();
|
|
|
|
const outcome = runPlaywrightA11y({
|
|
env: completeReadinessEnv({ E2E_REQUIRE_SUBSCRIPTION_SUCCESS: "1" }),
|
|
spawn,
|
|
stdout,
|
|
});
|
|
|
|
assert.deepEqual(outcome, { status: "fail", reason: "subscription-success-readiness-env-missing", exitCode: 1 });
|
|
assert.deepEqual(stdout.lines, [
|
|
"FAIL: E2E subscription success readiness env missing: E2E_TENANT_SUBSCRIPTION_SUCCESS_PATH while E2E_REQUIRE_SUBSCRIPTION_SUCCESS=1.",
|
|
]);
|
|
assert.equal(spawn.calls.filter((call) => call.command === "pnpm" && call.args[0] === "exec").length, 0);
|
|
});
|
|
|
|
it("fails before running Playwright when real workflow webhook execution lacks its target contract", () => {
|
|
const spawn = recordingSpawn({});
|
|
const stdout = captureLines();
|
|
|
|
assert.deepEqual(
|
|
missingWorkflowWebhookRunReadinessEnv({
|
|
env: { E2E_ALLOW_WORKFLOW_WEBHOOK_RUN: "1" },
|
|
}),
|
|
[
|
|
"E2E_TENANT_EMAIL",
|
|
"E2E_TENANT_PASSWORD",
|
|
"E2E_WORKFLOW_WEBHOOK_BUILDER_PATH",
|
|
"E2E_WORKFLOW_WEBHOOK_RUN_MANUAL_PATH",
|
|
"E2E_WORKFLOW_WEBHOOK_TARGET_URL",
|
|
]
|
|
);
|
|
assert.deepEqual(
|
|
missingWorkflowWebhookRunReadinessEnv({
|
|
env: completeReadinessEnv({
|
|
E2E_ALLOW_WORKFLOW_WEBHOOK_RUN: "1",
|
|
E2E_WORKFLOW_WEBHOOK_TARGET_URL: "https://hooks.example.test/readiness",
|
|
}),
|
|
}),
|
|
[]
|
|
);
|
|
|
|
const outcome = runPlaywrightA11y({
|
|
env: completeReadinessEnv({
|
|
E2E_ALLOW_WORKFLOW_WEBHOOK_RUN: "1",
|
|
E2E_WORKFLOW_WEBHOOK_TARGET_URL: "",
|
|
}),
|
|
spawn,
|
|
stdout,
|
|
});
|
|
|
|
assert.deepEqual(outcome, { status: "fail", reason: "workflow-webhook-run-readiness-env-missing", exitCode: 1 });
|
|
assert.deepEqual(stdout.lines, [
|
|
"FAIL: E2E workflow webhook run readiness env missing: E2E_WORKFLOW_WEBHOOK_TARGET_URL while E2E_ALLOW_WORKFLOW_WEBHOOK_RUN=1.",
|
|
]);
|
|
assert.equal(spawn.calls.length, 0);
|
|
});
|
|
|
|
it("fails before running Playwright when workflow outbox-drain readiness is misconfigured", () => {
|
|
const spawn = recordingSpawn({});
|
|
const stdout = captureLines();
|
|
|
|
assert.deepEqual(
|
|
missingWorkflowOutboxDrainReadinessEnv({
|
|
env: { E2E_REQUIRE_WORKFLOW_OUTBOX_DRAIN: "1", E2E_WORKFLOW_OUTBOX_DRAIN_EXPECTED_STATUS: "done" },
|
|
}),
|
|
[
|
|
"E2E_TENANT_EMAIL",
|
|
"E2E_TENANT_PASSWORD",
|
|
"E2E_WORKFLOW_RUN_MANUAL_PATH",
|
|
"E2E_WORKFLOW_OUTBOX_DRAIN_EXPECTED_STATUS=success|error",
|
|
]
|
|
);
|
|
assert.deepEqual(
|
|
missingWorkflowOutboxDrainReadinessEnv({
|
|
env: completeReadinessEnv({
|
|
E2E_REQUIRE_WORKFLOW_OUTBOX_DRAIN: "1",
|
|
E2E_WORKFLOW_OUTBOX_DRAIN_EXPECTED_STATUS: "error",
|
|
}),
|
|
}),
|
|
[]
|
|
);
|
|
|
|
const outcome = runPlaywrightA11y({
|
|
env: completeReadinessEnv({
|
|
E2E_REQUIRE_WORKFLOW_OUTBOX_DRAIN: "1",
|
|
E2E_WORKFLOW_OUTBOX_DRAIN_EXPECTED_STATUS: "done",
|
|
}),
|
|
spawn,
|
|
stdout,
|
|
});
|
|
|
|
assert.deepEqual(outcome, { status: "fail", reason: "workflow-outbox-drain-readiness-env-missing", exitCode: 1 });
|
|
assert.deepEqual(stdout.lines, [
|
|
"FAIL: E2E workflow outbox-drain readiness env missing: E2E_WORKFLOW_OUTBOX_DRAIN_EXPECTED_STATUS=success|error while E2E_REQUIRE_WORKFLOW_OUTBOX_DRAIN=1.",
|
|
]);
|
|
assert.equal(spawn.calls.length, 0);
|
|
});
|
|
|
|
it("static contract keeps subscription success strict mode explicit", () => {
|
|
const runnerSource = readFileSync("scripts/run-playwright-a11y.mjs", "utf8");
|
|
const subscriptionSource = readFileSync("tests/e2e/subscription-smoke.spec.ts", "utf8");
|
|
const readinessDocs = readFileSync("docs/testing/e2e-readiness.md", "utf8");
|
|
|
|
assert.match(runnerSource, /E2E_REQUIRE_SUBSCRIPTION_SUCCESS/u);
|
|
assert.match(runnerSource, /E2E_TENANT_SUBSCRIPTION_SUCCESS_PATH/u);
|
|
assert.match(subscriptionSource, /E2E_REQUIRE_SUBSCRIPTION_SUCCESS/u);
|
|
assert.match(
|
|
subscriptionSource,
|
|
/Subscription checkout success smoke requires E2E_TENANT_SUBSCRIPTION_SUCCESS_PATH while E2E_REQUIRE_SUBSCRIPTION_SUCCESS=1/u
|
|
);
|
|
assert.match(subscriptionSource, /follows checkout success return into the tenant dashboard on mobile when strict mode is enabled/u);
|
|
assert.match(subscriptionSource, /await expectTenantDashboard\(page\)/u);
|
|
assert.match(readinessDocs, /Normal\s+readiness runs stay non-mutating and do not require\s+`E2E_TENANT_SUBSCRIPTION_SUCCESS_PATH`/u);
|
|
assert.match(readinessDocs, /`E2E_REQUIRE_SUBSCRIPTION_SUCCESS=1`/u);
|
|
});
|
|
|
|
it("requires plugin settings and workflow fixtures separately from mutating opt-ins", () => {
|
|
assert.deepEqual(
|
|
missingReadinessEnv({
|
|
env: {
|
|
E2E_TENANT_EMAIL: "user@example.test",
|
|
E2E_TENANT_PASSWORD: "secret",
|
|
E2E_TENANT_DASHBOARD_PATH: "/app/acme/dashboard",
|
|
E2E_TENANT_SUBSCRIPTION_PATH: "/app/acme/settings/subscription",
|
|
E2E_WORKFLOW_RUN_MANUAL_PATH: "/app/acme/workflows/wf-1/run",
|
|
E2E_WORKFLOW_WEBHOOK_RUN_MANUAL_PATH: "/app/acme/workflows/wf-webhook/run",
|
|
PLUGIN_BINARY_BASE_PATH: "/tmp/e2e-plugins",
|
|
},
|
|
}),
|
|
[
|
|
"E2E_REGISTRATION_EXPECTS_EMAIL_VERIFICATION",
|
|
"E2E_TENANT_SETTINGS_ACCOUNT_PATH",
|
|
"E2E_TENANT_SETTINGS_PROFILE_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_WEBHOOK_BUILDER_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",
|
|
]
|
|
);
|
|
});
|
|
|
|
it("rejects invalid explicit registration email verification readiness assertions", () => {
|
|
assert.deepEqual(
|
|
missingReadinessEnv({
|
|
env: completeReadinessEnv({
|
|
E2E_REGISTRATION_EXPECTS_EMAIL_VERIFICATION: "yes",
|
|
}),
|
|
}),
|
|
["E2E_REGISTRATION_EXPECTS_EMAIL_VERIFICATION=0|1"]
|
|
);
|
|
});
|
|
|
|
it("keeps persistent plugin lifecycle E2E flows behind explicit opt-in", () => {
|
|
const source = readFileSync(
|
|
"tests/e2e/plugin-settings.mobile-a11y.spec.ts",
|
|
"utf8"
|
|
);
|
|
|
|
assert.match(source, /skipOrFailWhenE2eOptInMissing/u);
|
|
assert.match(source, /E2E_ALLOW_PLUGIN_LIFECYCLE_MUTATION/u);
|
|
assert.match(source, /E2E_ALLOW_PLUGIN_SETTINGS_WRITE/u);
|
|
assert.match(
|
|
source,
|
|
/test\("capability grant and revoke persist for the seeded readiness plugin"[\s\S]*?skipUnlessPluginLifecycleMutationAllowed\("capability grant\/revoke persistence flow"\)/u
|
|
);
|
|
assert.match(
|
|
source,
|
|
/test\("lifecycle controls persist enable and disable state for the seeded lifecycle plugin"[\s\S]*?skipUnlessPluginLifecycleMutationAllowed\("enable\/disable persistence flow"\)/u
|
|
);
|
|
});
|
|
|
|
it("keeps mutating registration and account E2E flows readiness-aware behind explicit opt-ins", () => {
|
|
const source = readFileSync(
|
|
"tests/e2e/registration-account-dashboard.mobile-a11y.spec.ts",
|
|
"utf8"
|
|
);
|
|
|
|
assert.match(source, /skipOrFailWhenE2eOptInMissing\("E2E_ALLOW_REGISTRATION_CREATE"/u);
|
|
assert.match(source, /skipOrFailWhenE2eOptInMissing\("E2E_ALLOW_ACCOUNT_CREATE"/u);
|
|
assert.match(source, /skipOrFailWhenE2eOptInMissing\("E2E_ALLOW_NO_TENANT_ACCOUNT_CREATE"/u);
|
|
assert.doesNotMatch(source, /test\.skip\(process\.env\.E2E_ALLOW_(?:REGISTRATION_CREATE|ACCOUNT_CREATE|NO_TENANT_ACCOUNT_CREATE)/u);
|
|
});
|
|
|
|
it("keeps the local mutating E2E readiness lane explicit and documented", () => {
|
|
const packageJson = JSON.parse(readFileSync("package.json", "utf8"));
|
|
const script = packageJson.scripts["verify:e2e-mutating-readiness:local"];
|
|
const docs = readFileSync("docs/testing/e2e-readiness.md", "utf8");
|
|
const requiredFlags = [
|
|
"REQUIRE_E2E_MUTATING_READINESS=1",
|
|
"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",
|
|
];
|
|
|
|
assert.equal(typeof script, "string");
|
|
assert.match(script, /pnpm run verify:e2e-a11y/u);
|
|
assert.match(docs, /verify:e2e-mutating-readiness:local/u);
|
|
for (const flag of requiredFlags) {
|
|
assert.match(script, new RegExp(flag, "u"));
|
|
assert.match(docs, new RegExp(flag, "u"));
|
|
}
|
|
assert.match(docs, /E2E_NO_TENANT_EMAIL/u);
|
|
assert.match(docs, /E2E_NO_TENANT_PASSWORD/u);
|
|
});
|
|
|
|
it("keeps the local worker-drain E2E readiness lane explicit and documented", () => {
|
|
const packageJson = JSON.parse(readFileSync("package.json", "utf8"));
|
|
const script = packageJson.scripts["verify:e2e-mutating-readiness:worker-local"];
|
|
const docs = readFileSync("docs/testing/e2e-readiness.md", "utf8");
|
|
|
|
assert.equal(typeof script, "string");
|
|
assert.match(script, /E2E_REQUIRE_WORKFLOW_OUTBOX_DRAIN=1/u);
|
|
assert.match(script, /pnpm run verify:e2e-mutating-readiness:local/u);
|
|
assert.match(docs, /verify:e2e-mutating-readiness:worker-local/u);
|
|
assert.match(docs, /E2E_REQUIRE_WORKFLOW_OUTBOX_DRAIN=1/u);
|
|
assert.match(docs, /WorkflowExecution\.status/u);
|
|
});
|
|
|
|
it("keeps real workflow webhook E2E runs behind explicit opt-in and target URL", () => {
|
|
const source = readFileSync(
|
|
"tests/e2e/workflow-builder-run.mobile-a11y.spec.ts",
|
|
"utf8"
|
|
);
|
|
|
|
assert.match(source, /E2E_ALLOW_WORKFLOW_WEBHOOK_RUN/u);
|
|
assert.match(source, /E2E_WORKFLOW_WEBHOOK_TARGET_URL/u);
|
|
assert.match(source, /E2E_WORKFLOW_WEBHOOK_BUILDER_PATH/u);
|
|
assert.match(source, /E2E_WORKFLOW_WEBHOOK_RUN_MANUAL_PATH/u);
|
|
assert.match(source, /E2E workflow webhook run opt-in missing/u);
|
|
assert.match(
|
|
source,
|
|
/if \(allowWorkflowWebhookRun\)[\s\S]*?requireWorkflowWebhookRunReadiness\(\)[\s\S]*?else[\s\S]*?skipOrFailWhenE2eInputsMissing/u
|
|
);
|
|
assert.match(
|
|
source,
|
|
/if \(allowWorkflowWebhookRun\)[\s\S]*?workflowWebhookTargetUrl![\s\S]*?else[\s\S]*?interceptCurrentRoutePost/u
|
|
);
|
|
});
|
|
|
|
it("runs Playwright when runtime and target are reachable", () => {
|
|
const spawn = recordingSpawn({
|
|
npx: ({ args }) => {
|
|
if (args[0] === "--no-install") {
|
|
return { status: 0, stdout: "Version 1.59.1", stderr: "" };
|
|
}
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
},
|
|
pnpm: () => ({ status: 0, stdout: "", stderr: "" }),
|
|
curl: () => ({ status: 0, stdout: "200", stderr: "" }),
|
|
});
|
|
const stdout = captureLines();
|
|
|
|
const outcome = runPlaywrightA11y({
|
|
env: { E2E_BASE_URL: "https://staging.example.test" },
|
|
spawn,
|
|
stdout,
|
|
});
|
|
|
|
assert.deepEqual(outcome, { status: "pass", reason: "ok", exitCode: 0 });
|
|
const runCall = spawn.calls.find((call) => call.command === "pnpm" && call.args[0] === "exec");
|
|
assert.ok(runCall, "expected playwright test to be invoked");
|
|
assert.deepEqual(runCall.args, ["exec", "playwright", "test", "--config=playwright.config.ts"]);
|
|
assert.deepEqual(runCall.options, { stdio: "inherit" });
|
|
});
|
|
|
|
it("returns the Playwright exit status when the suite fails", () => {
|
|
const spawn = recordingSpawn({
|
|
npx: ({ args }) => {
|
|
if (args[0] === "--no-install") {
|
|
return { status: 0, stdout: "Version 1.59.1", stderr: "" };
|
|
}
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
},
|
|
pnpm: () => ({ status: 3, stdout: "", stderr: "failed" }),
|
|
curl: () => ({ status: 0, stdout: "200", stderr: "" }),
|
|
});
|
|
|
|
const outcome = runPlaywrightA11y({ env: {}, spawn, stdout: captureLines() });
|
|
|
|
assert.deepEqual(outcome, { status: "fail", reason: "non-zero-exit", exitCode: 3 });
|
|
});
|
|
|
|
it("treats a valid curl status body as reachable", () => {
|
|
const spawn = recordingSpawn({
|
|
curl: () => ({ status: 0, stdout: "503", stderr: "" }),
|
|
});
|
|
|
|
assert.equal(isUrlReachable("https://degraded.example.test", { spawn }), true);
|
|
});
|
|
|
|
it("treats server errors as unreachable in strict readiness mode", () => {
|
|
const spawn = recordingSpawn({
|
|
npx: ({ args }) => {
|
|
if (args[0] === "--no-install") {
|
|
return { status: 0, stdout: "Version 1.59.1", stderr: "" };
|
|
}
|
|
return { status: 0, stdout: "", stderr: "" };
|
|
},
|
|
curl: () => ({ status: 0, stdout: "503", stderr: "" }),
|
|
});
|
|
const stdout = captureLines();
|
|
|
|
assert.equal(isUrlReachable("https://degraded.example.test", { spawn, allowServerErrors: false }), false);
|
|
|
|
const outcome = runPlaywrightA11y({
|
|
env: { E2E_BASE_URL: "https://degraded.example.test", REQUIRE_E2E_A11Y: "1" },
|
|
spawn,
|
|
stdout,
|
|
});
|
|
|
|
assert.deepEqual(outcome, { status: "fail", reason: "target-unreachable", exitCode: 1 });
|
|
assert.deepEqual(stdout.lines, ["FAIL: Target URL https://degraded.example.test not reachable while REQUIRE_E2E_A11Y=1."]);
|
|
assert.equal(spawn.calls.filter((call) => call.command === "pnpm").length, 0);
|
|
});
|
|
});
|