249 lines
9.3 KiB
TypeScript
249 lines
9.3 KiB
TypeScript
import { expect, test, type Page, type TestInfo } from "@playwright/test";
|
|
import { mockApi, primeMockSession } from "./support/network.js";
|
|
import { isDesktopProject } from "./support/projects";
|
|
|
|
const json = (body: unknown, 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 releaseRuntime = () => ({
|
|
generated_at: "2026-05-19T09:30:00.000Z",
|
|
trace_id: "trace-session-release-e2e",
|
|
channel: {
|
|
id: 2,
|
|
slug: "canary",
|
|
name: "Canary",
|
|
enabled: true,
|
|
default_channel: false,
|
|
},
|
|
versions: {
|
|
frontend: {
|
|
version_label: "frontend-canary",
|
|
commit_sha: "c0ffee0000001111222233334444555566667777",
|
|
repository: "truckwash/front-end-vue",
|
|
branch: "release/canary",
|
|
status: "deployed",
|
|
},
|
|
api: {
|
|
version_label: "api-canary",
|
|
commit_sha: "feedface00001111222233334444555566667777",
|
|
repository: "truckwash/backend-php",
|
|
branch: "release/canary",
|
|
status: "deployed",
|
|
},
|
|
service_set: {
|
|
id: 53,
|
|
name: "Canary isolated stack",
|
|
stack: {
|
|
frontend: { id: 51, label: "canary-frontend", coolify_service_uuid: "frontend-service-uuid" },
|
|
api: { id: 52, label: "canary-api", coolify_service_uuid: "api-service-uuid" },
|
|
database: { id: 54, resource_name: "canary-db", resource_uuid: "database-resource-uuid" },
|
|
redis: { id: 55, resource_name: "canary-redis", resource_uuid: "redis-resource-uuid" },
|
|
minio: { id: 56, resource_name: "canary-minio", resource_uuid: "minio-resource-uuid" },
|
|
},
|
|
},
|
|
bundle_id: 31,
|
|
bundle: { id: 31, version_label: "canary-bundle", status: "deployed" },
|
|
},
|
|
urls: {
|
|
frontend_base_url: "http://127.0.0.1:5173/canary/frontend",
|
|
api_base_url: "/api",
|
|
},
|
|
availability: {
|
|
configured: true,
|
|
missing: [],
|
|
status: "ready",
|
|
},
|
|
capture_policy: {
|
|
enabled: false,
|
|
capture_level: "metadata",
|
|
all_failure_metadata: true,
|
|
retention_days: 14,
|
|
},
|
|
});
|
|
|
|
async function openHiddenRuntimeMenu(page: Page) {
|
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
await page.keyboard.press("Shift");
|
|
await page.keyboard.press("Shift");
|
|
await page.keyboard.press("Shift");
|
|
|
|
if (
|
|
await page
|
|
.getByTestId("request-queue-runtime-box")
|
|
.isVisible({ timeout: 1000 })
|
|
.catch(() => false)
|
|
) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
await expect(page.getByTestId("request-queue-runtime-box")).toBeVisible();
|
|
}
|
|
|
|
async function bootAuthenticatedSession(page: Page) {
|
|
const runtime = releaseRuntime();
|
|
|
|
await page.addInitScript(() => {
|
|
window.localStorage.setItem("locale", "en");
|
|
window.localStorage.setItem("superuser_token", "release-runtime-superuser-token");
|
|
window.localStorage.setItem(
|
|
"release_channel_switch_notice_seen",
|
|
JSON.stringify({ "user:77:canary": "2026-05-19T09:30:00.000Z" })
|
|
);
|
|
});
|
|
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
sessionData: {
|
|
id: 77,
|
|
customer_number: 990077,
|
|
display_name: "Release Inspector",
|
|
runtime_config: {
|
|
release: runtime,
|
|
},
|
|
},
|
|
});
|
|
await page.route("**/release/runtime**", async (route) => {
|
|
await route.fulfill(json({ data: runtime }));
|
|
});
|
|
await primeMockSession(page, { token: "session-release-runtime-token", bootPath: "/user" });
|
|
}
|
|
|
|
test.describe("session release runtime inspector", () => {
|
|
test.beforeEach(async ({ page }, testInfo: TestInfo) => {
|
|
test.skip(!isDesktopProject(testInfo), "Hidden runtime menu coverage is desktop-focused.");
|
|
await bootAuthenticatedSession(page);
|
|
});
|
|
|
|
test("shows the active release and connected services in the Shift menu", async ({ page }) => {
|
|
await openHiddenRuntimeMenu(page);
|
|
|
|
const runtimeBox = page.getByTestId("request-queue-runtime-box");
|
|
await expect(runtimeBox).toContainText("Session release");
|
|
await expect(runtimeBox).toContainText("Canary");
|
|
await expect(runtimeBox).toContainText("trace-session-release-e2e");
|
|
await expect(runtimeBox).toContainText("#31 canary-bundle");
|
|
await expect(runtimeBox).toContainText("Canary isolated stack");
|
|
|
|
await expect(page.getByTestId("request-queue-release-app-frontend")).toContainText("frontend-canary");
|
|
await expect(page.getByTestId("request-queue-release-app-frontend")).toContainText(
|
|
"http://127.0.0.1:5173/canary/frontend"
|
|
);
|
|
await expect(page.getByTestId("request-queue-release-app-api")).toContainText("api-canary");
|
|
await expect(page.getByTestId("request-queue-release-app-api")).toContainText("/api");
|
|
await expect(page.getByTestId("request-queue-release-service-frontend")).toContainText("canary-frontend #51");
|
|
await expect(page.getByTestId("request-queue-release-service-api")).toContainText("canary-api #52");
|
|
await expect(page.getByTestId("request-queue-release-service-database")).toContainText("canary-db #54");
|
|
await expect(page.getByTestId("request-queue-release-service-redis")).toContainText("canary-redis #55");
|
|
await expect(page.getByTestId("request-queue-release-service-minio")).toContainText("canary-minio #56");
|
|
await expect(runtimeBox).toContainText("Runtime details");
|
|
await expect(page.getByTestId("request-queue-i18n-catalog-switch")).toHaveCount(0);
|
|
});
|
|
|
|
test("copies a redacted trace for request errors in the Shift menu", async ({ page }) => {
|
|
await page.addInitScript(() => {
|
|
Object.defineProperty(navigator, "clipboard", {
|
|
configurable: true,
|
|
value: {
|
|
writeText: async (text) => {
|
|
(window as unknown as { __copiedRequestErrorTrace: string }).__copiedRequestErrorTrace = text;
|
|
},
|
|
},
|
|
});
|
|
(window as unknown as { __copiedRequestErrorTrace: string }).__copiedRequestErrorTrace = "";
|
|
});
|
|
|
|
await page.route("**/debug/trace-copy", async (route) => {
|
|
const corsHeaders = {
|
|
"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",
|
|
"access-control-expose-headers": "Server-Timing, server-timing",
|
|
};
|
|
|
|
if (route.request().method() === "OPTIONS") {
|
|
await route.fulfill({
|
|
status: 204,
|
|
headers: corsHeaders,
|
|
body: "",
|
|
});
|
|
return;
|
|
}
|
|
|
|
await route.fulfill({
|
|
...json(
|
|
{
|
|
success: false,
|
|
data: {
|
|
message: "Trace copy backend failed",
|
|
secret: "raw-response-secret",
|
|
visible_response: "trace-copy-visible-response",
|
|
},
|
|
},
|
|
502
|
|
),
|
|
headers: {
|
|
...corsHeaders,
|
|
"content-type": "application/json",
|
|
"server-timing": "app;dur=123.400",
|
|
},
|
|
});
|
|
});
|
|
|
|
await page.goto("/__e2e/economic-queue?token=raw-active-url-token");
|
|
await expect(page.getByTestId("economic-queue-harness-page")).toBeVisible();
|
|
await page.getByTestId("economic-queue-harness-trace-copy-trigger").click();
|
|
await expect(page.getByTestId("economic-queue-harness-trace-copy-error")).toContainText(
|
|
"Trace copy backend failed"
|
|
);
|
|
|
|
await openHiddenRuntimeMenu(page);
|
|
const errorsBox = page.getByTestId("request-queue-errors-box");
|
|
await expect(errorsBox).toContainText("/debug/trace-copy");
|
|
const traceError = errorsBox
|
|
.locator(".request-queue-progress__error-item")
|
|
.filter({
|
|
hasText: "/debug/trace-copy",
|
|
})
|
|
.first();
|
|
await traceError.locator('[data-testid^="request-queue-copy-trace-"]').click();
|
|
await expect(traceError.locator('[data-testid^="request-queue-copy-trace-"]')).toContainText("Copied");
|
|
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(() => (window as unknown as { __copiedRequestErrorTrace: string }).__copiedRequestErrorTrace)
|
|
)
|
|
.toContain("Request Error Trace");
|
|
|
|
const copiedTrace = await page.evaluate(
|
|
() => (window as unknown as { __copiedRequestErrorTrace: string }).__copiedRequestErrorTrace
|
|
);
|
|
expect(copiedTrace).toContain("Active URL:");
|
|
expect(copiedTrace).toContain("token=[redacted]");
|
|
expect(copiedTrace).toContain("Initializing component: EconomicQueuePlaywrightHarness");
|
|
expect(copiedTrace).toContain("EconomicQueuePlaywrightHarness -> triggerTraceCopyFailure");
|
|
expect(copiedTrace).toContain("Method: POST");
|
|
expect(copiedTrace).toContain("Endpoint: /debug/trace-copy");
|
|
expect(copiedTrace).toContain('"authorization": "[redacted]"');
|
|
expect(copiedTrace).toContain("trace-copy-visible-request");
|
|
expect(copiedTrace).toContain("trace-copy-visible-response");
|
|
expect(copiedTrace).toContain("Server response time: app 123.4 ms");
|
|
expect(copiedTrace).toContain("Recreate:");
|
|
expect(copiedTrace).not.toContain("https://api-v2.truckwash.io/master/api");
|
|
expect(copiedTrace).not.toContain("raw-active-url-token");
|
|
expect(copiedTrace).not.toContain("trace-copy-body-token");
|
|
expect(copiedTrace).not.toContain("raw-response-secret");
|
|
});
|
|
});
|