e2e/perf-budgets.spec.ts
Linked repository file from this build.
import { expect, test, type Page, type Response } from "@playwright/test";
import { budgets, browserBudgets } from "../src/lib/perf/budgets";
import { meter, profiles, type NetworkProfile } from "./perf/measure";
// These selectors require data-bearing components, never just a page heading.
// Seeded data is a prerequisite; an empty fixture must not yield a deceptively fast result.
const components = [
{
name: "inbox",
path: "/dashboard",
api: "/api/inbox",
selector: 'main section ul li a[href^="/reviews/"]',
bytes: budgets.inboxApi,
},
{
name: "catalog",
path: "/reviews",
api: "/api/catalog",
selector: 'main section[aria-busy="false"] table[aria-label="Reviews in branch"] tbody a',
bytes: budgets.catalogApi,
},
{
name: "workspace",
path: "/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d",
api: "/api/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d",
selector: ".review-workspace #client-name",
bytes: budgets.oneReviewWithReceivedCopiesApi,
},
{
name: "search",
path: "/search",
api: "/api/search",
selector: 'main [aria-busy="false"] table[aria-label="Search results"] tbody a',
bytes: budgets.searchApi,
},
{
name: "reports",
path: "/reports",
api: "/api/reports",
selector: '.report-view[aria-busy="false"] button:not(:disabled):has(svg.lucide-play)',
bytes: budgets.reportsApi,
},
{
name: "exports",
path: "/exports",
api: "/api/exports",
selector: 'table[aria-label="Export eligibility by sample month"] tbody [role="switch"]',
bytes: budgets.exportsApi,
},
] as const;
async function verifyData(page: Page, name: string, response: Pick<Response, "ok" | "json">) {
expect(response.ok(), "component data request succeeds").toBe(true);
const data = await response.json();
const main = page.locator("main");
if (name === "workspace") {
expect(data.review.clientName.length, "workspace fixture has a real name").toBeGreaterThan(0);
await expect(main.locator("#client-name")).toHaveValue(data.review.clientName);
} else if (name === "catalog" || name === "search") {
expect(data.reviews.length, "measurement needs populated results").toBeGreaterThan(0);
await expect(
main.locator(`a[href="/reviews/${data.reviews[0].reviewKey}"]`).first(),
).toBeVisible();
} else if (name === "reports") {
expect(data.summary.total, "measurement needs populated reports").toBeGreaterThan(0);
const metric = page
.getByRole("heading", { name: "Reviews in scope", exact: true })
.locator("..")
.locator("p")
.first();
await expect(metric).toHaveText(data.summary.total.toLocaleString());
await expect(page.getByRole("button", { name: "Run report", exact: true })).toBeEnabled();
} else if (name === "exports") {
expect(data.months.length).toBeGreaterThan(0);
await expect(
page.getByRole("table", { name: "Export eligibility by sample month" }).locator("tbody tr"),
).toHaveCount(data.months.length);
} else {
const sections = main.locator("section[aria-busy]");
await expect(sections).toHaveCount(4);
await expect(main.locator('section[aria-busy="true"]')).toHaveCount(0);
const rows = [...data.newReviews, ...data.lastTouched, ...data.awaitingReview];
expect(rows.length, "inbox fixture must contain review tasks").toBeGreaterThan(0);
await expect(main.locator(`a[href="/reviews/${rows[0].reviewKey}"]`).first()).toBeVisible();
}
await expect(main.getByRole("alert")).toHaveCount(0);
return {
records:
name === "reports"
? data.summary.total
: name === "workspace"
? 1
: name === "exports"
? data.months.length
: name === "inbox"
? data.totals
: data.total,
};
}
for (const component of components) {
for (const mode of ["cold", "navigation"] as const) {
test(`${mode}: ${component.name} data to rendered component`, async ({
page,
baseURL,
}, info) => {
const profile = (info.project.metadata.profile as NetworkProfile | undefined) ?? profiles[0];
// The regular E2E config also runs these tests, without synthetic latency.
if (!process.env.QC_PERF_BASE_URL)
await page.context().addCookies([{ name: "qc-test-staff", value: "501", url: baseURL! }]);
const measure = await meter(page, profile);
let action = () => page.goto(component.path, { waitUntil: "commit" });
if (mode === "navigation") {
if (component.name === "workspace") {
await page.goto("/search?q=60001");
await expect(
page.locator('table[aria-label="Search results"] tbody a').first(),
).toBeVisible();
action = async () => {
await page
.locator('main a[href="/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d"]')
.first()
.click();
return null;
};
} else {
await page.goto(component.name === "inbox" ? "/reviews" : "/dashboard");
await expect(
page
.locator(component.name === "inbox" ? components[1].selector : components[0].selector)
.first(),
).toBeVisible();
action =
component.name === "search"
? async () => {
await page
.getByRole("textbox", { name: "Search reviews", exact: true })
.press("Enter");
return null;
}
: async () => {
await page.locator(`a[href="${component.path}"]:visible`).first().click();
return null;
};
}
// Include normal source-page prefetch in setup; record its separate resource cost below.
await page.waitForTimeout(1_000);
await info.attach("navigation-setup", {
body: JSON.stringify(
await page.evaluate(() => {
const entries = performance.getEntriesByType(
"resource",
) as PerformanceResourceTiming[];
return {
transferBytes: entries.reduce((sum, entry) => sum + entry.transferSize, 0),
resources: entries.length,
};
}),
),
contentType: "application/json",
});
}
const sourceTimeOrigin = await page.evaluate(() => performance.timeOrigin);
const responses: Response[] = [];
page.on("response", (response) => {
if (
new URL(response.url()).pathname === component.api &&
response.request().method() === "GET"
)
responses.push(response);
});
await measure.begin(component.selector, mode === "cold", component.path);
await action();
const result = await measure.finish(info, component.name, mode, component.api);
expect(result.readinessError).toBe("");
if (mode === "navigation")
expect(
await page.evaluate(() => performance.timeOrigin),
"navigation must stay in the same document",
).toBe(sourceTimeOrigin);
expect
.soft(result.smoothness?.visibilityRegressions, "ready content must not disappear again")
.toBe(0);
expect
.soft(result.smoothness?.settledMs, "content remains fully visible at observation end")
.not.toBeNull();
expect
.soft(
result.smoothness?.animations.filter((a) =>
["page-enter", "content-loaded"].includes(a.name),
),
"no repeated zero-opacity route/data fades",
)
.toEqual([]);
const responsiveInbox = component.name === "inbox" && mode === "cold";
expect(responses.length, "server data avoids a duplicate initial GET").toBe(
responsiveInbox ? 1 : 0,
);
// Independent API verification happens after the measured window. Server data is
// delivered in HTML/RSC and counted in wire bytes, not fabricated as API transfer.
const verificationUrl = new URL(component.api, baseURL);
if (component.name === "reports") {
verificationUrl.searchParams.set(
"from",
await page.getByLabel("From sample month", { exact: true }).inputValue(),
);
verificationUrl.searchParams.set(
"through",
await page.getByLabel("Through sample month", { exact: true }).inputValue(),
);
}
const dataset = await verifyData(
page,
component.name,
responses.at(-1) ?? (await page.request.get(verificationUrl.toString())),
);
await info.attach("data-delivery", {
body: JSON.stringify(responsiveInbox ? "server+responsive" : "server"),
contentType: "application/json",
});
await info.attach("dataset", {
body: JSON.stringify(dataset),
contentType: "application/json",
});
const limits = browserBudgets[profile.name === "local" ? "local" : "internet"];
expect
.soft(result.componentMs, `${component.name} data visible (ms)`)
.toBeLessThan(limits[mode]);
expect
.soft(
result.wireBytes,
"wire bytes, including background downloads for 1 second after readiness",
)
.toBeLessThan(browserBudgets.wireBytes[mode]);
const dataRequests = result.requests.filter(
(row) => row.path === component.api && row.method === "GET",
);
expect
.soft(dataRequests.length, "one component load should not duplicate its data GET")
.toBe(responsiveInbox ? 1 : 0);
const payloads = result.apis.filter(
(row) => row.path === component.api && row.method === "GET",
);
expect
.soft(payloads.length, "initial data is delivered with the route")
.toBe(responsiveInbox ? 1 : 0);
for (const payload of payloads)
expect
.soft(payload.jsonBytes, `${component.api} decoded bytes`)
.toBeLessThan(component.bytes);
expect
.soft(result.apiJsonBytes, "all APIs combined, including shell/background requests")
.toBeLessThan(browserBudgets.apiJsonBytes);
});
}
}
// Reuse one document and session: isolated journeys cannot expose repeat-navigation churn.
test("repeated navigation stays visible after each loading handoff", async ({ page }, info) => {
test.setTimeout(120_000);
const profile = (info.project.metadata.profile as NetworkProfile | undefined) ?? profiles[0];
const measure = await meter(page, profile);
await page.goto("/dashboard");
await expect(page.locator(components[0].selector).first()).toBeVisible();
for (let lap = 0; lap < 2; lap++) {
for (const name of ["catalog", "reports", "exports", "inbox"]) {
const component = components.find((entry) => entry.name === name)!;
await measure.begin(component.selector, false, component.path);
await page.locator(`a[href="${component.path}"]:visible`).first().click();
const result = await measure.finish(
info,
`${name}-lap-${lap + 1}`,
"navigation",
component.api,
);
expect.soft(result.readinessError).toBe("");
expect.soft(result.smoothness?.visibilityRegressions).toBe(0);
expect.soft(result.smoothness?.settledMs).not.toBeNull();
expect
.soft(
result.smoothness?.animations.filter((a) =>
["page-enter", "content-loaded"].includes(a.name),
),
)
.toEqual([]);
}
}
});