import { expect, test } from "@playwright/test"; import { mockApi, seedAuthenticatedState } from "./support/network.js"; const json = (body, status = 200) => ({ status, contentType: "application/json", headers: { "access-control-allow-origin": "*", "access-control-allow-headers": "Content-Type, Authorization, X-Customer-Number, X-Release-Trace, X-Release-Channel, X-Frontend-Version, *", "access-control-allow-methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS", }, body: JSON.stringify(body), }); const GUARD_TIMEOUT_MS = 30_000; const unavailableRuntime = { generated_at: "2026-05-19T09:00:00.000Z", trace_id: "trace-unavailable-channel", channel: { id: 2, slug: "canary", name: "Canary", description: "Early production validation before broader rollout.", enabled: true, default_channel: false, }, versions: { frontend: null, api: null, bundle_id: null }, availability: { configured: false, missing: ["release_bundle", "frontend_version", "api_version"], status: "unconfigured", }, capture_policy: { enabled: false, capture_level: "metadata", all_failure_metadata: true, retention_days: 14, }, }; const availableRuntime = () => ({ ...unavailableRuntime, versions: { frontend: { version_label: "frontend-canary", commit_sha: "c0ffee" }, api: { version_label: "api-canary", commit_sha: "feedface" }, }, availability: { configured: true, missing: [], status: "ready", }, }); const runtimeWithSelectableReleaseDetails = () => ({ ...unavailableRuntime, versions: { frontend: null, api: null, bundle_id: null, }, availability: { configured: false, missing: ["api_base_url"], status: "unconfigured", }, available_channels: [ { channel: { id: 1, slug: "stable", name: "Stable", description: "Standard production channel.", enabled: true, default_channel: true, }, versions: { frontend: { version_label: "frontend-stable", commit_sha: "abc1234567890000111122223333444455556666", deployed_at: "2026-05-18T07:45:00.000Z", }, api: { version_label: "api-stable", commit_sha: "def456789abc0000111122223333444455556666", deployed_at: "2026-05-18T07:47:00.000Z", }, bundle_id: 31, bundle: { id: 31, promoted_at: "2026-05-18T08:00:00.000Z", }, }, availability: { configured: true, missing: [], status: "ready", }, }, { channel: unavailableRuntime.channel, versions: { frontend: { version_label: "frontend-canary", commit_sha: "c0ffee0000001111222233334444555566667777", deployed_at: "2026-05-19T08:15:00.000Z", }, api: { version_label: "api-canary", commit_sha: "feedface00001111222233334444555566667777", deployed_at: "2026-05-19T08:18:00.000Z", }, bundle_id: null, }, availability: { configured: false, missing: ["api_base_url"], status: "unconfigured", }, }, ], }); const runtimeWithFailingBetaSwitch = () => ({ generated_at: "2026-05-19T09:10:00.000Z", trace_id: "trace-stable-channel", channel: { id: 1, slug: "stable", name: "Stable", description: "Standard production channel.", enabled: true, default_channel: true, }, versions: { frontend: { version_label: "frontend-stable", commit_sha: "abc123" }, api: { version_label: "api-stable", commit_sha: "def456" }, bundle_id: 31, }, availability: { configured: true, missing: [], status: "ready", }, available_channels: [ { channel: { id: 1, slug: "stable", name: "Stable", description: "Standard production channel.", enabled: true, default_channel: true, }, availability: { configured: true, missing: [], status: "ready", }, }, { channel: { id: 3, slug: "beta", name: "Beta", description: "Beta validation channel.", enabled: true, default_channel: false, }, availability: { configured: true, missing: [], status: "ready", }, }, ], }); async function boot(page, runtime = unavailableRuntime, locale = "en") { await page.addInitScript((selectedLocale) => { window.localStorage.setItem("locale", selectedLocale); window.localStorage.removeItem("release_channel_unavailable_ignore_until"); window.localStorage.setItem("release_source_override", "deployment"); }, locale); await mockApi(page, { authenticated: true, permissions: ["user"], sessionData: { runtime_config: { release: runtime, }, }, }); await seedAuthenticatedState(page, "release-channel-token"); } test("users assigned to an unconfigured release channel can ignore the guard temporarily", async ({ page }) => { await boot(page); await page.route("**/release/runtime", async (route) => { await route.fulfill(json({ data: unavailableRuntime })); }); await page.goto("/user", { waitUntil: "domcontentloaded" }); const guard = page.getByTestId("release-channel-unavailable-page"); await expect(guard).toBeVisible({ timeout: GUARD_TIMEOUT_MS }); await expect(guard).toContainText("Release channel is not ready"); await expect(guard).toContainText("Canary"); await expect(page.getByTestId("release-channel-missing")).not.toContainText("Release bundle"); await expect(page.getByTestId("release-channel-missing")).toContainText("Frontend version"); await expect(page.getByTestId("release-channel-missing")).toContainText("API version"); await expect(page.getByTestId("release-channel-next-check")).toContainText("Checking again in"); await page.getByTestId("release-channel-ignore").click(); await expect(guard).toHaveCount(0); await page.evaluate(() => window.localStorage.removeItem("release_channel_unavailable_ignore_until")); }); test("invalid release runtime JSON shows the unavailable guard instead of crashing bootstrap", async ({ page }) => { const internalRuntime = { ...unavailableRuntime, channel: { ...unavailableRuntime.channel, slug: "internal", name: "Internal", default_channel: false, }, versions: { frontend: null, api: null, bundle_id: null }, availability: { configured: false, missing: ["release_runtime"], status: "unconfigured", }, }; const consoleErrors = []; page.on("console", (message) => { if (message.type() === "error") { consoleErrors.push(message.text()); } }); await boot(page, internalRuntime); await page.addInitScript(() => { window.localStorage.setItem("release_channel_selected_slug", "internal"); window.localStorage.setItem("release_source_override", "deployment"); }); await page.route("**/release/runtime**", async (route) => { await route.fulfill({ status: 200, contentType: "text/html", headers: { "access-control-allow-origin": "*", "access-control-allow-headers": "Content-Type, Authorization, X-Customer-Number, X-Release-Trace, X-Release-Channel, X-Frontend-Version, *", "access-control-allow-methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS", }, body: '
Warning Composer autoload warning {"success":true}', }); }); await page.goto("/user", { waitUntil: "domcontentloaded" }); const guard = page.getByTestId("release-channel-unavailable-page"); await expect(guard).toBeVisible({ timeout: GUARD_TIMEOUT_MS }); await expect(guard).toContainText("Internal"); await expect(page.getByTestId("release-channel-missing")).toContainText("Release runtime"); expect(consoleErrors.join("\n")).not.toContain("Cannot read properties of null"); }); test("selected channel auth session 404 shows the release channel guard", async ({ page }) => { const internalRuntime = { ...availableRuntime(), channel: { ...unavailableRuntime.channel, slug: "internal", name: "Internal", default_channel: false, }, frontend_base_url: null, api_base_url: "https://api-v2.truckwash.io/internal/api", urls: { frontend_base_url: null, api_base_url: "https://api-v2.truckwash.io/internal/api", }, availability: { configured: true, missing: [], status: "ready", }, }; const channelApiRequests = []; await boot(page, internalRuntime); await page.addInitScript(() => { window.localStorage.setItem("release_channel_selected_slug", "internal"); window.localStorage.setItem("release_source_override", "deployment"); }); await page.route("**/release/runtime**", async (route) => { await route.fulfill(json({ data: internalRuntime })); }); await page.route("https://api-v2.truckwash.io/internal/api/auth/session**", async (route) => { channelApiRequests.push(route.request().url()); await route.fulfill(json({ data: { message: "Not found" } }, 404)); }); await page.goto("/user", { waitUntil: "domcontentloaded" }); const guard = page.getByTestId("release-channel-unavailable-page"); await expect(guard).toBeVisible({ timeout: GUARD_TIMEOUT_MS }); await expect(guard).toContainText("Internal"); await expect(page.getByTestId("release-channel-missing")).toContainText("API URL"); await expect(page.locator(".swal2-popup")).toHaveCount(0); await expect.poll(() => channelApiRequests.length).toBeGreaterThan(0); expect(channelApiRequests.every((url) => url.startsWith("https://api-v2.truckwash.io/internal/api/"))).toBe(true); }); test("release channel choices show git commit and release time when available", async ({ page }) => { const runtime = runtimeWithSelectableReleaseDetails(); await boot(page, runtime); await page.route("**/release/runtime", async (route) => { await route.fulfill(json({ data: runtime })); }); await page.goto("/user", { waitUntil: "domcontentloaded" }); const guard = page.getByTestId("release-channel-unavailable-page"); await expect(guard).toBeVisible({ timeout: GUARD_TIMEOUT_MS }); const canary = page.getByTestId("release-channel-option-canary"); await expect(canary).toBeVisible(); await expect(canary).not.toContainText("Release bundle"); await expect(page.getByTestId("release-channel-option-canary-frontend-release")).toContainText("c0ffee000000"); await expect(page.getByTestId("release-channel-option-canary-frontend-release")).toContainText( /May 19, 2026|19 May 2026/ ); await expect(page.getByTestId("release-channel-option-canary-api-release")).toContainText("feedface0000"); await expect(page.getByTestId("release-channel-option-canary-api-release")).toContainText(/May 19, 2026|19 May 2026/); await expect(page.getByTestId("release-channel-option-stable-bundle-release")).toContainText("#31"); await expect(page.getByTestId("release-channel-option-stable-bundle-release")).toContainText( /May 18, 2026|18 May 2026/ ); }); test("failed sidebar release channel switches keep the previous channel active", async ({ page }, testInfo) => { test.skip(testInfo.project.name.includes("mobile"), "The sidebar release selector is hidden in the mobile layout."); const runtime = runtimeWithFailingBetaSwitch(); await boot(page, runtime); const runtimeRequests = []; await page.route("**/release/runtime**", async (route) => { const url = new URL(route.request().url()); const selectedChannel = url.searchParams.get("release_channel") || ""; runtimeRequests.push(selectedChannel || "default"); if (selectedChannel === "beta") { await route.fulfill(json({ data: { message: "Beta runtime unavailable" } }, 502)); return; } await route.fulfill(json({ data: runtime })); }); await page.goto("/user", { waitUntil: "domcontentloaded" }); const stable = page.getByTestId("release-channel-option-stable"); const beta = page.getByTestId("release-channel-option-beta"); await expect(stable).toBeVisible({ timeout: GUARD_TIMEOUT_MS }); await expect(stable).toHaveAttribute("aria-pressed", "true"); await expect(beta).toHaveAttribute("aria-pressed", "false"); await beta.click(); await expect(stable).toHaveAttribute("aria-pressed", "true"); await expect(beta).toHaveAttribute("aria-pressed", "false"); await expect(page.getByRole("alert")).toContainText("previous channel is still active"); await expect.poll(() => page.evaluate(() => window.localStorage.getItem("release_channel_selected_slug"))).toBeNull(); expect(runtimeRequests).toContain("beta"); }); test("predefined release channel text is localized on the guard page", async ({ page }) => { const internalRuntime = { ...unavailableRuntime, channel: { ...unavailableRuntime.channel, slug: "internal", name: "Internal", description: "Internal staff and superuser validation channel.", }, versions: { frontend: { version_label: "frontend-internal", commit_sha: "130cc2fc106a1111222233334444555566667777", deployed_at: "2026-05-20T09:32:00.000Z", }, api: { version_label: "api-internal", commit_sha: "24ac681365511111222233334444555566667777", deployed_at: "2026-05-20T09:32:00.000Z", }, bundle_id: 10, bundle: { id: 10, promoted_at: "2026-05-20T09:33:00.000Z", }, }, availability: { configured: false, missing: ["frontend_base_url"], status: "unconfigured", }, }; await boot(page, internalRuntime, "da"); await page.route("**/release/runtime", async (route) => { await route.fulfill(json({ data: internalRuntime })); }); await page.goto("/user", { waitUntil: "domcontentloaded" }); const guard = page.getByTestId("release-channel-unavailable-page"); await expect(guard).toBeVisible({ timeout: GUARD_TIMEOUT_MS }); await expect(guard).toContainText("Release-kanalen er ikke klar"); await expect(guard).toContainText("Intern"); await expect(guard).toContainText("Intern kanal til medarbejdere"); await expect(page.getByTestId("release-channel-missing")).toContainText("Frontend-URL"); await expect(page.getByTestId("release-channel-missing")).not.toContainText("frontend_base_url"); await expect(guard).not.toContainText("Internal staff"); await expect(guard).not.toContainText(/\binternal\b/); await expect(guard).not.toContainText(/\p{L}\?\p{L}|\?\p{L}/u); }); test("users assigned to an unconfigured release channel can check again when it becomes ready", async ({ page }) => { await boot(page); let releaseRuntime = unavailableRuntime; await page.route("**/release/runtime", async (route) => { await route.fulfill(json({ data: releaseRuntime })); }); await page.goto("/user", { waitUntil: "domcontentloaded" }); const guard = page.getByTestId("release-channel-unavailable-page"); await expect(guard).toBeVisible({ timeout: GUARD_TIMEOUT_MS }); const currentUrl = page.url(); releaseRuntime = availableRuntime(); await page.getByTestId("release-channel-check-again").click(); await expect(guard).toHaveCount(0); await expect(page).toHaveURL(currentUrl); });