feat: harden test and registry evidence

This commit is contained in:
donghyeon-ka
2026-07-26 17:15:26 +09:00
parent 3f634eb655
commit 98d4fd4960
68 changed files with 6167 additions and 250 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test";
import { expect, test } from "../support/browser/strict-browser-test.js";
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.js";
for (const route of Object.values(ROUTE_REGISTRY).map((definition) => {
+15 -1
View File
@@ -1,4 +1,5 @@
import { expect, test } from "@playwright/test";
import { expect, test } from "../support/browser/strict-browser-test.js";
import { successEnvelope } from "../mocks/contracts/envelopes.js";
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.js";
test("boots the public app shell", async ({ page }) => {
@@ -32,6 +33,19 @@ test("opens the protected integration route through the local demo seam", async
(definition) => definition.access === "integration-defined",
);
if (!protectedRoute) throw new Error("An integration route is required");
await page.route(
"http://localhost:8080/api/reference-resources?*",
(route) =>
route.fulfill({
json: successEnvelope([
{
id: "browser-reference",
name: "Browser reference",
createdAt: "2026-07-26T00:00:00.000Z",
},
]),
}),
);
await page.goto(protectedRoute.path);
await expect(
page.getByRole("heading", { name: "세션이 필요합니다." }),
+17
View File
@@ -0,0 +1,17 @@
import { expect, test } from "../support/browser/strict-browser-test.js";
test("boots and navigates the compact production shell", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("main")).toBeVisible();
const menu = page.getByRole("button", { name: "메뉴", exact: true });
await expect(menu).toHaveCSS("min-width", "44px");
await menu.click();
await expect(page.getByRole("navigation", { name: "주요 탐색" })).toBeVisible();
await page.getByRole("link", { name: "UI 구성요소" }).click();
await expect(page).toHaveURL(/\/examples\/ui$/);
await expect(page.locator("html")).toHaveAttribute("data-build-id", "local-build");
const overflow = await page.evaluate(
() => document.documentElement.scrollWidth - window.innerWidth,
);
expect(overflow).toBeLessThanOrEqual(1);
});
+1 -1
View File
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test } from "../support/browser/strict-browser-test.js";
test("runs menu typeahead, tabs and duplicate toast interactions", async ({
page,
+1 -1
View File
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test } from "../support/browser/strict-browser-test.js";
test("switches the shell locale and keeps pseudo-locale copy within compact layout", async ({
page,
+1 -1
View File
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test } from "../support/browser/strict-browser-test.js";
/** @param {import("@playwright/test").Page} page */
async function openReferenceForm(page) {
+1 -1
View File
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test } from "../support/browser/strict-browser-test.js";
test("reflows the UI gallery at the 320px minimum without horizontal overflow", async ({
page,
+1 -1
View File
@@ -1,5 +1,5 @@
import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "@playwright/test";
import { expect, test } from "../support/browser/strict-browser-test.js";
test("persists an explicit color scheme through the storage contract", async ({
page,
+1 -1
View File
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test";
import { expect, test } from "../support/browser/strict-browser-test.js";
test("validates and reports the common text-field flow", async ({ page }) => {
await page.goto("/examples/ui");
@@ -1,7 +1,5 @@
// @vitest-environment jsdom
import { HttpResponse, http } from "msw";
import { setupServer } from "msw/node";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {
@@ -16,6 +14,9 @@ import {
import { createRuntimeComposition } from "../../../src/bootstrap/create-runtime-composition.js";
import { RuntimeApplication } from "../../../src/bootstrap/runtime-application.jsx";
import { createBootstrapHandlers } from "../../mocks/handlers/bootstrap.js";
import { createReferenceScenarioHandlers } from "../../mocks/handlers/reference-resources.js";
import { createStrictMockServer } from "../../mocks/server.js";
const runtimeConfig = {
APP_ENV: "local",
@@ -56,41 +57,23 @@ const releaseManifest = {
const listRequests = vi.fn();
const createRequests = vi.fn();
const resources = [{ id: "reference-1", name: "Existing" }];
const server = setupServer(
http.get("http://app.test/config.json", () =>
HttpResponse.json(runtimeConfig),
),
http.get("http://app.test/release-manifest.json", () =>
HttpResponse.json(releaseManifest),
),
http.get("https://api.test/api/reference-resources", ({ request }) => {
listRequests(new URL(request.url).search);
return HttpResponse.json({
success: true,
data: resources,
meta: { requestId: "request-list", traceId: "trace-list" },
});
}),
http.post("https://api.test/api/reference-resources", async ({ request }) => {
const body = (await request.json()) as { name: string };
createRequests(body);
const created = { id: "reference-created", name: body.name };
resources.push(created);
return HttpResponse.json({
success: true,
data: created,
meta: { requestId: "request-create", traceId: "trace-create" },
});
const mockApi = createStrictMockServer(
...createBootstrapHandlers(runtimeConfig, releaseManifest),
...createReferenceScenarioHandlers({
resources,
onList: listRequests,
onCreate: createRequests,
}),
);
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
beforeAll(mockApi.listen);
afterEach(() => {
mockApi.reset();
listRequests.mockClear();
createRequests.mockClear();
resources.splice(1);
});
afterAll(() => server.close());
afterAll(mockApi.close);
const absoluteFetch: typeof fetch = (input, init) => {
if (input instanceof Request) return fetch(input, init);
+225
View File
@@ -0,0 +1,225 @@
{
"schemaVersion": 1,
"cases": [
{
"id": "ordering-only",
"expected": "none",
"before": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": {
"A": { "path": "/a", "tags": ["one", "two"] },
"B": { "path": "/b", "tags": ["three"] }
}
}
]
},
"after": {
"registries": [
{
"rows": {
"B": { "tags": ["three"], "path": "/b" },
"A": { "tags": ["two", "one"], "path": "/a" }
},
"contract": { "breakingFields": ["path"] },
"registryId": "FE-REG-TEST"
}
],
"schemaVersion": 2
}
},
{
"id": "row-addition",
"expected": "additive",
"before": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": { "A": { "path": "/a" } }
}
]
},
"after": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": {
"A": { "path": "/a" },
"B": { "path": "/b" }
}
}
]
}
},
{
"id": "behavior-change",
"expected": "behavior-change",
"before": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": { "A": { "path": "/a", "owner": "one" } }
}
]
},
"after": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": { "A": { "path": "/a", "owner": "two" } }
}
]
}
},
{
"id": "row-removal",
"expected": "breaking",
"before": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": {
"A": { "path": "/a" },
"B": { "path": "/b" }
}
}
]
},
"after": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": { "A": { "path": "/a" } }
}
]
}
},
{
"id": "field-type-narrowing",
"expected": "breaking",
"before": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": { "A": { "limit": 10 } }
}
]
},
"after": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": { "A": { "limit": "10" } }
}
]
}
},
{
"id": "route-path-change",
"expected": "breaking",
"before": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": { "A": { "path": "/before" } }
}
]
},
"after": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": { "A": { "path": "/after" } }
}
]
}
},
{
"id": "registry-contract-narrowing",
"expected": "breaking",
"before": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": {
"breakingFields": ["path"],
"allowedValues": { "kind": ["one", "two"] }
},
"rows": { "A": { "path": "/a", "kind": "one" } }
}
]
},
"after": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": {
"breakingFields": ["path"],
"allowedValues": { "kind": ["one"] }
},
"rows": { "A": { "path": "/a", "kind": "one" } }
}
]
}
}
],
"breakingEvidence": {
"before": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": { "A": { "path": "/before" } }
}
]
},
"after": {
"schemaVersion": 2,
"registries": [
{
"registryId": "FE-REG-TEST",
"contract": { "breakingFields": ["path"] },
"rows": { "A": { "path": "/after" } }
}
]
}
},
"tamperedApproval": {
"snapshot": {
"schemaVersion": 2,
"registries": []
},
"approval": {
"schemaVersion": 1,
"snapshotDigest": "tampered",
"owner": "fixture-owner",
"approvedAt": "2026-07-26T00:00:00.000Z"
}
}
}
@@ -0,0 +1,6 @@
{
"schemaVersion": 1,
"snapshotDigest": "0000000000000000000000000000000000000000000000000000000000000000",
"owner": "negative-fixture",
"approvedAt": "2026-07-26T00:00:00.000Z"
}
@@ -0,0 +1,5 @@
test("false visual pass", async ({ page }) => {
await expect(page).toHaveScreenshot({
mask: [page.locator("body")]
});
});
@@ -0,0 +1 @@
test.skip("unowned indefinite quarantine", async () => {});
+30
View File
@@ -0,0 +1,30 @@
export function successEnvelope<Value>(
data: Value,
requestId = "fixture-request",
) {
return Object.freeze({
success: true as const,
data: structuredClone(data),
meta: Object.freeze({
requestId,
traceId: "fixture-trace",
}),
});
}
export function failureEnvelope(
code: string,
details?: Readonly<Record<string, unknown>>,
) {
return Object.freeze({
success: false as const,
error: Object.freeze({
code,
...(details ? { details: structuredClone(details) } : {}),
}),
meta: Object.freeze({
requestId: "fixture-request",
traceId: "fixture-trace",
}),
});
}
+16
View File
@@ -0,0 +1,16 @@
import { http, HttpResponse } from "msw";
export function createBootstrapHandlers(
runtimeConfig: Readonly<Record<string, unknown>>,
releaseManifest: Readonly<Record<string, unknown>>,
baseUrl = "http://app.test",
) {
return [
http.get(`${baseUrl}/config.json`, () =>
HttpResponse.json(structuredClone(runtimeConfig)),
),
http.get(`${baseUrl}/release-manifest.json`, () =>
HttpResponse.json(structuredClone(releaseManifest)),
),
] as const;
}
+171
View File
@@ -0,0 +1,171 @@
import { delay, http, HttpResponse } from "msw";
import {
failureEnvelope,
successEnvelope,
} from "../contracts/envelopes.js";
import type { HttpScenarioId } from "../scenarios/catalog.js";
import { assertOperationScenario } from "../scenarios/catalog.js";
export type ReferenceResourceFixture = Readonly<{
id: string;
name: string;
createdAt?: string;
}>;
type ScenarioOptions = Readonly<{
baseUrl?: string;
scenarios?: Partial<
Record<
| "LIST_REFERENCE_RESOURCES"
| "CREATE_REFERENCE_RESOURCE"
| "GET_REFERENCE_RESOURCE",
HttpScenarioId
>
>;
resources?: ReferenceResourceFixture[];
onList?(search: string): void;
onCreate?(body: Readonly<Record<string, unknown>>): void;
}>;
const DEFAULT_RESOURCE = Object.freeze({
id: "reference-1",
name: "Reference",
createdAt: "2026-07-26T00:00:00.000Z",
});
async function scenarioResponse(
scenario: HttpScenarioId,
payload: unknown,
attempt: number,
) {
if (scenario === "slow") await delay(50);
if (scenario === "timeout") await delay(30_000);
if (scenario === "network-error") return HttpResponse.error();
if (scenario === "content-type-mismatch") {
return new HttpResponse("<html>not json</html>", {
headers: { "Content-Type": "text/html" },
});
}
if (scenario === "malformed-json") {
return new HttpResponse("{invalid", {
headers: { "Content-Type": "application/json" },
});
}
if (scenario === "envelope-mismatch") {
return HttpResponse.json({ data: payload });
}
if (scenario === "schema-mismatch") {
return HttpResponse.json(successEnvelope({ unexpected: true }));
}
if (
scenario === "auth-persistent-401" ||
(scenario === "auth-recover-once" && attempt === 1)
) {
return HttpResponse.json(failureEnvelope("AUTH_REQUIRED"), {
status: 401,
});
}
if (scenario === "forbidden-403") {
return HttpResponse.json(failureEnvelope("FORBIDDEN"), { status: 403 });
}
if (scenario === "not-found-404") {
return HttpResponse.json(failureEnvelope("NOT_FOUND"), { status: 404 });
}
if (scenario === "conflict-409") {
return HttpResponse.json(failureEnvelope("CONFLICT"), { status: 409 });
}
if (scenario === "validation-422") {
return HttpResponse.json(
failureEnvelope("VALIDATION_REJECTED", {
issues: [{ path: "name", code: "too_small" }],
}),
{ status: 422 },
);
}
if (scenario === "rate-limited-429") {
return HttpResponse.json(failureEnvelope("RATE_LIMITED"), {
status: 429,
headers: { "Retry-After": "1" },
});
}
if (
scenario === "server-terminal-500" ||
(scenario === "server-retry-success" && attempt === 1)
) {
return HttpResponse.json(failureEnvelope("SERVER_FAILURE"), {
status: 503,
});
}
return HttpResponse.json(successEnvelope(payload));
}
export function createReferenceScenarioHandlers(options: ScenarioOptions = {}) {
const baseUrl = options.baseUrl ?? "https://api.test";
const resources = options.resources ?? [{ ...DEFAULT_RESOURCE }];
const attempts = new Map<string, number>();
const scenarioFor = (
operationId: keyof NonNullable<ScenarioOptions["scenarios"]>,
) =>
assertOperationScenario(
operationId,
options.scenarios?.[operationId] ?? "success",
);
const nextAttempt = (operationId: string) => {
const next = (attempts.get(operationId) ?? 0) + 1;
attempts.set(operationId, next);
return next;
};
return [
http.get(`${baseUrl}/api/reference-resources`, ({ request }) => {
options.onList?.(new URL(request.url).search);
const scenario = scenarioFor("LIST_REFERENCE_RESOURCES");
const payload = scenario === "empty" ? [] : resources;
return scenarioResponse(
scenario,
payload,
nextAttempt("LIST_REFERENCE_RESOURCES"),
);
}),
http.post(
`${baseUrl}/api/reference-resources`,
async ({ request }) => {
const rawBody = await request.json();
const body =
rawBody &&
typeof rawBody === "object" &&
!Array.isArray(rawBody)
? (rawBody as Readonly<Record<string, unknown>>)
: {};
options.onCreate?.(body);
const scenario = scenarioFor("CREATE_REFERENCE_RESOURCE");
const created = {
id: "reference-created",
name: String(body.name ?? "Created"),
createdAt: "2026-07-26T00:00:00.000Z",
};
if (scenario === "success") resources.push(created);
return scenarioResponse(
scenario,
created,
nextAttempt("CREATE_REFERENCE_RESOURCE"),
);
},
),
http.get(
`${baseUrl}/api/reference-resources/:resourceId`,
({ params }) => {
const scenario = scenarioFor("GET_REFERENCE_RESOURCE");
const resource =
resources.find((entry) => entry.id === params.resourceId) ??
DEFAULT_RESOURCE;
return scenarioResponse(
scenario,
resource,
nextAttempt("GET_REFERENCE_RESOURCE"),
);
},
),
] as const;
}
+45
View File
@@ -0,0 +1,45 @@
export const HTTP_SCENARIO_IDS = Object.freeze([
"success",
"empty",
"slow",
"network-error",
"timeout",
"aborted",
"content-type-mismatch",
"malformed-json",
"envelope-mismatch",
"schema-mismatch",
"auth-recover-once",
"auth-persistent-401",
"forbidden-403",
"not-found-404",
"conflict-409",
"validation-422",
"rate-limited-429",
"server-retry-success",
"server-terminal-500",
] as const);
export type HttpScenarioId = (typeof HTTP_SCENARIO_IDS)[number];
export const OPERATION_SCENARIO_CATALOG = Object.freeze({
LIST_REFERENCE_RESOURCES: HTTP_SCENARIO_IDS,
CREATE_REFERENCE_RESOURCE: Object.freeze(
HTTP_SCENARIO_IDS.filter(
(scenario) => !["empty", "aborted"].includes(scenario),
),
),
GET_REFERENCE_RESOURCE: HTTP_SCENARIO_IDS,
} satisfies Readonly<Record<string, readonly HttpScenarioId[]>>);
export function assertOperationScenario(
operationId: keyof typeof OPERATION_SCENARIO_CATALOG,
scenario: HttpScenarioId,
) {
if (!OPERATION_SCENARIO_CATALOG[operationId].includes(scenario)) {
throw new Error(
`Scenario ${scenario} is not declared for ${operationId}`,
);
}
return scenario;
}
+13
View File
@@ -0,0 +1,13 @@
import { setupServer } from "msw/node";
export function createStrictMockServer(
...handlers: Parameters<typeof setupServer>
) {
const server = setupServer(...handlers);
return Object.freeze({
server,
listen: () => server.listen({ onUnhandledRequest: "error" }),
reset: () => server.resetHandlers(),
close: () => server.close(),
});
}
+24
View File
@@ -0,0 +1,24 @@
import AxeBuilder from "@axe-core/playwright";
import {
expect,
test,
} from "../support/browser/strict-browser-test.js";
test("runs the isolated overlay interaction with blocking accessibility", async ({
page,
}) => {
await page.goto(
"/iframe.html?id=platform-design-system--overlay-interaction&viewMode=story",
);
const trigger = page.getByRole("button", { name: "Open dialog" });
await expect(trigger).toBeVisible();
await trigger.click();
await expect(
page.getByRole("dialog", { name: "Confirm platform action" }),
).toBeVisible();
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
await page.keyboard.press("Escape");
await expect(trigger).toBeFocused();
});
@@ -0,0 +1,46 @@
import {
expect,
test as base,
type ConsoleMessage,
type Page,
} from "@playwright/test";
function ignoredConsole(message: ConsoleMessage) {
return (
message.type() === "warning" &&
message.text().includes("NO_COLOR")
);
}
export const test = base.extend({
page: async ({ page }, use) => {
const failures: string[] = [];
const onConsole = (message: ConsoleMessage) => {
if (
["error", "warning"].includes(message.type()) &&
!ignoredConsole(message)
) {
failures.push(`console:${message.type()}:${message.text()}`);
}
};
const onPageError = (error: Error) => {
failures.push(`pageerror:${error.name}`);
};
const onRequestFailed = (request: import("@playwright/test").Request) => {
failures.push(
`requestfailed:${request.method()}:${new URL(request.url()).pathname}:${request.failure()?.errorText ?? "unknown"}`,
);
};
page.on("console", onConsole);
page.on("pageerror", onPageError);
page.on("requestfailed", onRequestFailed);
await use(page);
page.off("console", onConsole);
page.off("pageerror", onPageError);
page.off("requestfailed", onRequestFailed);
expect(failures, "unexpected browser console/page errors").toEqual([]);
},
});
export { expect };
export type { Page };
+69
View File
@@ -0,0 +1,69 @@
import { vi } from "vitest";
export function createDeterministicClock(start = 0) {
let current = start;
return Object.freeze({
now: () => current,
sleep: async (milliseconds: number, signal?: AbortSignal) => {
if (signal?.aborted) throw new DOMException("aborted", "AbortError");
current += Math.max(0, milliseconds);
},
advance(milliseconds: number) {
current += Math.max(0, milliseconds);
},
});
}
export function createSeededRandom(seed = 1) {
let state = seed >>> 0;
return () => {
state = (state * 1_664_525 + 1_013_904_223) >>> 0;
return state / 0x1_0000_0000;
};
}
export function createControlledScheduler() {
const callbacks: Array<() => void> = [];
return Object.freeze({
callbacks,
setTimeout: vi.fn((callback: () => void) => {
callbacks.push(callback);
return callbacks.length - 1;
}),
clearTimeout: vi.fn(),
runNext() {
callbacks.shift()?.();
},
});
}
export function createRecordingStorage() {
const values = new Map<string, string>();
const operations: Array<
Readonly<{ operation: "get" | "set" | "remove"; key: string }>
> = [];
const storage: Storage = {
getItem(key) {
operations.push({ operation: "get", key });
return values.get(key) ?? null;
},
setItem(key, value) {
operations.push({ operation: "set", key });
values.set(key, value);
},
removeItem(key) {
operations.push({ operation: "remove", key });
values.delete(key);
},
clear() {
values.clear();
},
key(index) {
return [...values.keys()][index] ?? null;
},
get length() {
return values.size;
},
};
return Object.freeze({ storage, operations, values });
}
+102
View File
@@ -0,0 +1,102 @@
import { describe, expect, it } from "vitest";
import {
canonicalRegistryJson,
diffRegistrySnapshots,
registrySnapshotDigest,
validateBreakingEvidence,
verifyRegistryBaselineApproval,
} from "../../scripts/lib/registry-compatibility.mjs";
function snapshot(
rows: Readonly<Record<string, Readonly<Record<string, unknown>>>>,
) {
return {
schemaVersion: 2,
registries: [
{
registryId: "FE-REG-TEST",
contract: { breakingFields: ["path"] },
rows,
},
],
};
}
describe("registry compatibility evidence", () => {
it("canonicalizes object and primitive-array ordering", () => {
expect(
canonicalRegistryJson({
second: ["b", "a"],
first: { z: 1, a: 2 },
}),
).toBe('{"first":{"a":2,"z":1},"second":["a","b"]}');
});
it("classifies additive, behavioral and breaking actual diffs", () => {
expect(
diffRegistrySnapshots(
snapshot({ A: { path: "/a" } }),
snapshot({ A: { path: "/a" }, B: { path: "/b" } }),
).impact,
).toBe("additive");
expect(
diffRegistrySnapshots(
snapshot({ A: { path: "/a", owner: "one" } }),
snapshot({ A: { path: "/a", owner: "two" } }),
).impact,
).toBe("behavior-change");
expect(
diffRegistrySnapshots(
snapshot({ A: { path: "/a" } }),
snapshot({ A: { path: "/moved" } }),
).impact,
).toBe("breaking");
expect(
diffRegistrySnapshots(
snapshot({ A: { path: "/a" } }),
snapshot({}),
).impact,
).toBe("breaking");
});
it("verifies the approved digest and complete breaking evidence", () => {
const before = snapshot({ A: { path: "/before" } });
const after = snapshot({ A: { path: "/after" } });
const digest = registrySnapshotDigest(before);
expect(
verifyRegistryBaselineApproval(before, {
schemaVersion: 1,
snapshotDigest: digest,
owner: "platform",
approvedAt: "2026-07-26T00:00:00.000Z",
}).passed,
).toBe(true);
expect(
verifyRegistryBaselineApproval(before, {
schemaVersion: 1,
snapshotDigest: "tampered",
owner: "platform",
approvedAt: "2026-07-26T00:00:00.000Z",
}).passed,
).toBe(false);
const diff = diffRegistrySnapshots(before, after);
expect(validateBreakingEvidence(diff, { changes: [] }).passed).toBe(false);
const changeId = diff.changes[0]?.changeId;
expect(
validateBreakingEvidence(diff, {
changes: [
{
changeId,
versionBump: "2",
migration: "dual-read",
compatibilityWindow: "one release",
rollback: "restore previous registry snapshot",
owner: "platform",
},
],
}).passed,
).toBe(true);
});
});
+18 -10
View File
@@ -2,24 +2,32 @@ import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
describe("registry governance manifest", () => {
it("declares exactly nine single-owner registries and impact labels", async () => {
it("declares ten typed, single-owner executable registries", async () => {
const governance = JSON.parse(
await readFile("config/contracts/registry-governance.json", "utf8"),
);
const registries =
/** @type {Array<{registryId: string, owner: string}>} */ (
/** @type {Array<{
* registryId: string,
* owner: string,
* requiredFields: string[],
* fieldTypes: Record<string, string>
* }>} */ (
governance.registries
);
expect(governance.registries).toHaveLength(9);
expect(governance.registries).toHaveLength(10);
expect(new Set(registries.map((entry) => entry.registryId)).size).toBe(
9,
10,
);
expect(registries.every((entry) => entry.owner)).toBe(true);
expect(governance.compatibilityImpact.allowed).toEqual([
"none",
"additive",
"behavior-change",
"breaking",
]);
expect(
registries.every(
(entry) =>
Array.isArray(entry.requiredFields) &&
entry.requiredFields.length > 0 &&
entry.fieldTypes &&
Object.keys(entry.fieldTypes).length > 0,
),
).toBe(true);
});
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

+43
View File
@@ -0,0 +1,43 @@
import {
expect,
test,
} from "../support/browser/strict-browser-test.js";
test("wide application shell visual contract", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto("/");
await expect(page.getByRole("main")).toBeVisible();
await expect(page).toHaveScreenshot("app-shell-wide-light.png", {
fullPage: true,
});
});
test("compact drawer and pseudo-locale visual contract", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/");
await page.locator("#locale-preference").selectOption("en-XA");
await page.locator(".app-shell__menu-button").click();
await expect(page.locator(".ui-drawer")).toHaveJSProperty("open", true);
await expect(page).toHaveScreenshot("app-shell-compact-pseudo-drawer.png", {
fullPage: true,
});
});
test("design-system gallery dark visual contract", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 900 });
await page.goto("/examples/ui");
await page.locator("#theme-preference").selectOption("dark");
await expect(page.getByRole("main")).toHaveScreenshot(
"design-system-gallery-dark.png",
);
});
test("loading empty error and access surfaces visual contract", async ({
page,
}) => {
await page.setViewportSize({ width: 1280, height: 1100 });
await page.goto("/examples/states");
await expect(page.getByRole("main")).toHaveScreenshot(
"state-surfaces-light.png",
);
});