Files
pleno-vue/tests/e2e/release/release.live-smoke.spec.ts
T

158 lines
5.9 KiB
TypeScript

import { expect, test, type APIRequestContext } from "@playwright/test";
import crypto from "node:crypto";
import { attachPageHealthGuards, expectBodyHasContent, settlePage } from "./helpers";
type ReleaseManifest = {
build_id?: string;
commit_sha?: string;
entry?: string;
css?: string[];
index_asset_urls?: string[];
pwa_asset_urls?: string[];
asset_urls?: string[];
asset_hashes?: Record<string, { sha256?: string; bytes?: number }>;
};
const baseURL = process.env.PLAYWRIGHT_BASE_URL || "";
const staticBaseURL = process.env.PLAYWRIGHT_RELEASE_STATIC_BASE_URL || process.env.RELEASE_BASE_URL || baseURL;
const appBaseURL = process.env.PLAYWRIGHT_APP_BASE_URL || baseURL;
function liveUrl(assetPath: string, base = staticBaseURL) {
const normalizedBase = base.endsWith("/") ? base : `${base}/`;
return new URL(assetPath.replace(/^\/+/, ""), normalizedBase).toString();
}
function unique(values: string[]) {
return Array.from(new Set(values.filter(Boolean)));
}
function sha256(bytes: Buffer) {
return crypto.createHash("sha256").update(bytes).digest("hex");
}
function rejectsHtml(assetPath: string) {
return /\.(?:js|css|json|webmanifest|svg|png|ico|woff2?|mp3)$/i.test(assetPath);
}
async function expectJson<T>(request: APIRequestContext, assetPath: string): Promise<T> {
const response = await request.get(liveUrl(assetPath), {
headers: {
"Cache-Control": "no-cache",
Pragma: "no-cache",
},
});
const body = await response.text();
expect(response.status(), `${assetPath} status`).toBe(200);
expect(response.headers()["content-type"] || "", `${assetPath} content-type`).not.toContain("text/html");
expect(body.trim().length, `${assetPath} body`).toBeGreaterThan(0);
return JSON.parse(body) as T;
}
async function expectStaticAsset(
request: APIRequestContext,
assetPath: string,
expectedHash?: { sha256?: string; bytes?: number }
) {
const response = await request.get(liveUrl(assetPath), {
headers: {
"Cache-Control": "no-cache",
Pragma: "no-cache",
},
});
const body = await response.body();
const contentType = response.headers()["content-type"] || "";
expect(response.status(), `${assetPath} status`).toBe(200);
expect(body.length, `${assetPath} body`).toBeGreaterThan(0);
if (rejectsHtml(assetPath)) {
expect(contentType, `${assetPath} content-type`).not.toContain("text/html");
}
if (expectedHash?.bytes !== undefined) {
expect(body.length, `${assetPath} bytes`).toBe(expectedHash.bytes);
}
if (expectedHash?.sha256) {
expect(sha256(body), `${assetPath} sha256`).toBe(expectedHash.sha256);
}
}
async function expectApiPing(request: APIRequestContext, apiBaseUrl: string, path: string) {
const url = new URL(path.replace(/^\/+/, ""), apiBaseUrl.endsWith("/") ? apiBaseUrl : `${apiBaseUrl}/`).toString();
const response = await request.get(url, {
headers: {
"Cache-Control": "no-cache",
Pragma: "no-cache",
},
});
const body = await response.text();
expect(response.status(), `${url} status`).toBe(200);
expect(response.headers()["content-type"] || "", `${url} content-type`).toMatch(/json/i);
expect(body.trim().length, `${url} body`).toBeGreaterThan(0);
expect(JSON.parse(body), `${url} JSON`).toMatchObject({ success: true });
}
test("@public-live release manifest, shell, and static assets are available", async ({ request }) => {
const manifest = await expectJson<ReleaseManifest>(request, "release-manifest.json");
const releaseEntry = await expectJson<{ entry?: string; css?: string[] }>(request, "release-entry.json");
expect(manifest.build_id, "release-manifest.json build_id").toBeTruthy();
expect(manifest.commit_sha, "release-manifest.json commit_sha").toBeTruthy();
expect(releaseEntry.entry, "release-entry.json entry").toBe(manifest.entry);
expect(releaseEntry.css || [], "release-entry.json css").toEqual(manifest.css || []);
for (const shellPath of ["/", "/guest/book/wash"]) {
const response = await request.get(liveUrl(shellPath), {
headers: {
"Cache-Control": "no-cache",
Pragma: "no-cache",
},
});
const body = await response.text();
expect(response.status(), `${shellPath} status`).toBe(200);
expect(response.headers()["content-type"] || "", `${shellPath} content-type`).toContain("text/html");
expect(body.replace(/\s+/g, "").length, `${shellPath} body`).toBeGreaterThan(40);
expect(body, `${shellPath} bootstrap loader`).toContain('data-testid="app-bootstrap-loading"');
}
const assetPaths = unique([
"release-manifest.json",
"release-entry.json",
manifest.entry || "",
...(manifest.css || []),
...(manifest.index_asset_urls || []),
...(manifest.pwa_asset_urls || []),
...(manifest.asset_urls || []),
]).filter((assetPath) => assetPath !== "/index.html");
for (const assetPath of assetPaths) {
const hashKey = assetPath.startsWith("/") ? assetPath : `/${assetPath}`;
await expectStaticAsset(request, assetPath, manifest.asset_hashes?.[assetPath] || manifest.asset_hashes?.[hashKey]);
}
});
test("@public-live api-v2 gateway and channel API prefixes serve JSON ping responses", async ({ request }) => {
const apiBaseUrl = process.env.PLAYWRIGHT_RELEASE_API_BASE_URL || "https://api-v2.truckwash.io";
const apiPingPaths = unique(
(process.env.PLAYWRIGHT_RELEASE_API_PING_PATHS || "/master/api/ping").split(",").map((value) => value.trim())
);
for (const apiPingPath of apiPingPaths) {
await expectApiPing(request, apiBaseUrl, apiPingPath);
}
});
test("@public-live guest flow renders on the deployed environment", async ({ page }) => {
const guards = attachPageHealthGuards(page, appBaseURL);
await page.goto(liveUrl("/guest/book/wash", appBaseURL));
await settlePage(page);
await expect(page).toHaveURL(/\/guest\/book\/wash(?:\?.*)?$/);
await expectBodyHasContent(page);
await expect(page.locator("body")).not.toContainText(/404/i);
await guards.expectHealthy();
guards.dispose();
});