chore: initialize from frontend template 4dc033c

This commit is contained in:
DongHyeonka
2026-08-13 18:23:26 +09:00
commit 40107eec84
897 changed files with 234824 additions and 0 deletions
@@ -0,0 +1,331 @@
import { expect, test } from "../support/browser/strict-browser-test.ts";
test("selects, inspects, streams, and previews a real browser File", async ({
page,
}) => {
await page.goto("/");
await page.evaluate(async () => {
const modulePath =
"/src/adapters/browser-files/index.ts";
const {
createBrowserFileRuntime,
browserFilePolicyReference,
} = await import(/* @vite-ignore */ modulePath);
const input = document.createElement("input");
input.id = "capability-file-input";
input.type = "file";
const label = document.createElement("label");
label.htmlFor = input.id;
label.textContent = "PNG file";
const button = document.createElement("button");
button.type = "button";
button.textContent = "Choose test file";
document.body.append(label, input, button);
const filePolicy = browserFilePolicyReference(
"avatar",
"select-inspect-preview-png",
);
const runtime = createBrowserFileRuntime({
input,
policies: [
{
reference: filePolicy,
selection: {
policyId: "avatar-v1",
purpose: "avatar",
classification: "PERSONAL",
multiple: false,
maxCount: 1,
maxFileBytes: 1024,
maxTotalBytes: 1024,
allowEmpty: false,
accept: [
{
mediaType: "image/png",
extensions: [".png"],
},
],
},
inspection: {
policyId: "png-signature-v1",
maxInspectionBytes: 16,
acceptedSignatures: [
{
mediaType: "image/png",
extensions: [".png"],
patterns: [
{
offset: 0,
bytes: [137, 80, 78, 71, 13, 10, 26, 10],
},
],
},
],
},
preview: {
allowedMediaTypes: ["image/png"],
maxPreviewBytes: 1024,
},
},
],
limits: {
hardMaxPreviewBytes: 1024,
hardMaxObjectUrlBytes: 1024,
hardMaxTransferBytes: 4096,
},
download: {
host: Object.freeze({ handoff() {} }),
browserManagedCapabilities: {
resolve() {
return {
ok: false as const,
error: {
code: "POLICY_REJECTED" as const,
operation: "DOWNLOAD" as const,
retryable: false,
recovery: "NONE" as const,
},
};
},
},
},
});
const state = globalThis as typeof globalThis & {
__browserFileResult?: Promise<unknown>;
};
button.addEventListener("click", () => {
state.__browserFileResult = (async () => {
const selected = await runtime.baselinePicker.select({
policy: filePolicy,
});
if (
!selected.ok ||
selected.value.kind !== "SELECTED" ||
selected.value.files.length !== 1
) {
runtime.dispose();
return selected;
}
const candidate = selected.value.files[0]!;
const signal = new AbortController().signal;
const inspected = await runtime.content.inspect({
ref: candidate.ref,
policy: filePolicy,
signal,
});
const range = await runtime.content.readRange({
ref: candidate.ref,
offset: 1,
length: 4,
signal,
});
const opened = await runtime.content.openSource({
ref: candidate.ref,
signal,
});
const streamed: number[] = [];
if (opened.ok) {
for await (const chunk of opened.value.stream(signal)) {
if (chunk.ok) streamed.push(...chunk.value);
}
}
const verificationReceipt = inspected.ok
? inspected.value.verificationReceipt
: null;
if (!verificationReceipt) {
runtime.dispose();
return inspected;
}
const preview = await runtime.previews.create({
ref: candidate.ref,
verificationReceipt,
policy: filePolicy,
maxPreviewBytes: 1024,
signal,
});
let previewBytes: number[] = [];
let previewProtocol: string | null = null;
if (preview.ok) {
previewProtocol = new URL(preview.value.url).protocol;
previewBytes = Array.from(
new Uint8Array(await (await fetch(preview.value.url)).arrayBuffer()),
);
preview.value.release();
}
runtime.baselinePicker.release(candidate.ref);
runtime.dispose();
return {
selected,
inspected,
range: range.ok ? Array.from(range.value) : range,
streamed,
preview: preview.ok ? { ok: true } : preview,
previewBytes,
previewProtocol,
};
})();
});
});
const chooserPromise = page.waitForEvent("filechooser");
await page.getByRole("button", { name: "Choose test file" }).click();
const chooser = await chooserPromise;
const bytes = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10, 1, 2, 3, 4]);
await chooser.setFiles({
name: "sample.png",
mimeType: "image/png",
buffer: bytes,
});
const result = await page.evaluate(async () => {
const state = globalThis as typeof globalThis & {
__browserFileResult?: Promise<unknown>;
};
return await state.__browserFileResult;
});
expect(result).toMatchObject({
selected: {
ok: true,
value: {
kind: "SELECTED",
files: [{ sizeBytes: bytes.byteLength, source: "NATIVE_INPUT" }],
},
},
inspected: {
ok: true,
value: {
byteLength: bytes.byteLength,
detectedMediaType: "image/png",
signature: "MATCHED",
},
},
range: [80, 78, 71, 13],
streamed: Array.from(bytes),
preview: { ok: true },
previewBytes: Array.from(bytes),
previewProtocol: "blob:",
});
});
test("hands a bounded generated Blob to the real browser download path", async ({
page,
}) => {
await page.goto("/");
await page.evaluate(async () => {
const modulePath = "/src/adapters/browser-files/index.ts";
const {
createBrowserFileRuntime,
createAnchorDownloadHost,
browserFilePolicyReference,
} = await import(/* @vite-ignore */ modulePath);
const input = document.createElement("input");
input.type = "file";
input.id = "unused-download-input";
const label = document.createElement("label");
label.htmlFor = input.id;
label.textContent = "Unused file control";
const button = document.createElement("button");
button.type = "button";
button.textContent = "Download generated file";
document.body.append(label, input, button);
const downloadPolicy = browserFilePolicyReference(
"generated-export",
"bounded-object-url-bin",
);
const runtime = createBrowserFileRuntime({
input,
policies: [
{
reference: downloadPolicy,
download: {
strategy: "BOUNDED_OBJECT_URL",
mediaType: "application/octet-stream",
safeExtension: ".bin",
maxTransferBytes: 1024,
maxBufferedBytes: 1024,
integrity: "OPTIONAL",
},
},
],
limits: {
hardMaxPreviewBytes: 1024,
hardMaxObjectUrlBytes: 1024,
hardMaxTransferBytes: 4096,
},
download: {
host: createAnchorDownloadHost(document),
browserManagedCapabilities: {
resolve() {
return {
ok: false as const,
error: {
code: "POLICY_REJECTED" as const,
operation: "DOWNLOAD" as const,
retryable: false,
recovery: "NONE" as const,
},
};
},
},
},
});
const state = globalThis as typeof globalThis & {
__browserDownloadResult?: Promise<unknown>;
};
button.addEventListener("click", () => {
state.__browserDownloadResult = runtime.downloads
.deliver({
policy: downloadPolicy,
source: {
kind: "GENERATED",
bytes: {
byteLength: 5,
async *stream() {
yield {
ok: true as const,
value: new Uint8Array([1, 2]),
};
yield {
ok: true as const,
value: new Uint8Array([3, 4, 5]),
};
},
},
},
suggestedFileName: "report",
maxTransferBytes: 1024,
maxBufferedBytes: 1024,
signal: new AbortController().signal,
onProgress() {},
})
.finally(() => runtime.dispose());
});
});
const downloadPromise = page.waitForEvent("download");
await page
.getByRole("button", { name: "Download generated file" })
.click();
const download = await downloadPromise;
const stream = await download.createReadStream();
const chunks: Buffer[] = [];
for await (const chunk of stream) {
chunks.push(Buffer.from(chunk));
}
const result = await page.evaluate(async () => {
const state = globalThis as typeof globalThis & {
__browserDownloadResult?: Promise<unknown>;
};
return await state.__browserDownloadResult;
});
expect(download.suggestedFilename()).toBe("report.bin");
expect(Buffer.concat(chunks)).toEqual(Buffer.from([1, 2, 3, 4, 5]));
expect(result).toMatchObject({
ok: true,
value: {
kind: "BROWSER_HANDOFF",
},
});
});
@@ -0,0 +1,290 @@
import {
expect,
test,
type Page,
} from "../support/browser/strict-browser-test.ts";
const TOPIC = "sample-topic-alpha";
const TOPIC_VERSION = 1;
const STATE_KEY = "__crossContextInvalidationCapability";
const STORAGE_PULSE_KEY =
"ca-frontend:cache-invalidation:v1:pulse";
type BrowserTransportMode = "BROADCAST" | "STORAGE";
type BrowserDeliverySnapshot = Readonly<{
ordering: string;
sequence: number;
topic: string;
transport: string;
}>;
type BrowserRuntimeSnapshot = Readonly<{
status: string;
deliveries: readonly BrowserDeliverySnapshot[];
pulseRetained: boolean;
}>;
async function installRuntime(
page: Page,
mode: BrowserTransportMode,
cacheEpoch: string,
): Promise<string> {
return await page.evaluate(
async ({
currentCacheEpoch,
currentMode,
stateKey,
topic,
topicVersion,
}) => {
const modulePath =
"/src/adapters/cross-context-invalidation/index.ts";
const { createBrowserCrossContextInvalidationFromHost } =
(await import(
/* @vite-ignore */ modulePath
)) as typeof import("../../src/adapters/cross-context-invalidation/index.ts");
const host: Record<string, unknown> =
currentMode === "BROADCAST"
? (globalThis as unknown as Record<string, unknown>)
: {
crypto: globalThis.crypto,
localStorage: globalThis.localStorage,
addEventListener:
globalThis.addEventListener.bind(globalThis),
removeEventListener:
globalThis.removeEventListener.bind(globalThis),
};
const runtime = createBrowserCrossContextInvalidationFromHost({
host,
cacheEpoch: currentCacheEpoch,
topics: [{ topic, topicVersion }],
});
if (!runtime) {
throw new Error(
"Cross-context invalidation runtime is unavailable.",
);
}
const deliveries: BrowserDeliverySnapshot[] = [];
runtime.subscribe((delivery) => {
deliveries.push({
ordering: delivery.ordering,
sequence: delivery.event.sequence,
topic: delivery.event.topic,
transport: delivery.transport,
});
});
Reflect.set(globalThis, stateKey, { deliveries, runtime });
return runtime.getStatus();
},
{
currentCacheEpoch: cacheEpoch,
currentMode: mode,
stateKey: STATE_KEY,
topic: TOPIC,
topicVersion: TOPIC_VERSION,
},
);
}
async function publish(page: Page) {
return await page.evaluate(
({ stateKey, topic, topicVersion }) => {
const state = Reflect.get(globalThis, stateKey) as
| {
runtime: {
publish(input: {
topic: string;
topicVersion: number;
}): unknown;
};
}
| undefined;
if (!state) throw new Error("Capability runtime is not installed.");
return state.runtime.publish({ topic, topicVersion });
},
{
stateKey: STATE_KEY,
topic: TOPIC,
topicVersion: TOPIC_VERSION,
},
);
}
async function snapshot(page: Page): Promise<BrowserRuntimeSnapshot> {
return await page.evaluate(
({ pulseKey, stateKey }) => {
const state = Reflect.get(globalThis, stateKey) as
| {
deliveries: BrowserDeliverySnapshot[];
runtime: { getStatus(): string };
}
| undefined;
if (!state) throw new Error("Capability runtime is not installed.");
return {
status: state.runtime.getStatus(),
deliveries: structuredClone(state.deliveries),
pulseRetained: localStorage.getItem(pulseKey) !== null,
};
},
{ pulseKey: STORAGE_PULSE_KEY, stateKey: STATE_KEY },
);
}
async function closeRuntime(page: Page): Promise<string | null> {
return await page.evaluate((stateKey) => {
const state = Reflect.get(globalThis, stateKey) as
| { runtime: { close(): void; getStatus(): string } }
| undefined;
if (!state) return null;
state.runtime.close();
return state.runtime.getStatus();
}, STATE_KEY);
}
async function disposeRuntime(page: Page): Promise<void> {
await page.evaluate(
({ pulseKey, stateKey }) => {
const state = Reflect.get(globalThis, stateKey) as
| { runtime: { close(): void } }
| undefined;
state?.runtime.close();
localStorage.removeItem(pulseKey);
Reflect.deleteProperty(globalThis, stateKey);
},
{ pulseKey: STORAGE_PULSE_KEY, stateKey: STATE_KEY },
);
}
test("delivers invalidation through native BroadcastChannel and cleans the receiver", async ({
page,
}) => {
const secondPage = await page.context().newPage();
const cacheEpoch = `browser.primary.${Date.now()}`;
try {
await Promise.all([page.goto("/"), secondPage.goto("/")]);
await page.evaluate(
(pulseKey) => localStorage.removeItem(pulseKey),
STORAGE_PULSE_KEY,
);
const statuses = await Promise.all([
installRuntime(page, "BROADCAST", cacheEpoch),
installRuntime(secondPage, "BROADCAST", cacheEpoch),
]);
expect(statuses).toEqual([
"ACTIVE_BROADCAST",
"ACTIVE_BROADCAST",
]);
expect(await publish(page)).toEqual({
ok: true,
transport: "BROADCAST",
});
await expect
.poll(async () => (await snapshot(secondPage)).deliveries)
.toEqual([
{
ordering: "NEXT",
sequence: 1,
topic: TOPIC,
transport: "BROADCAST",
},
]);
expect((await snapshot(page)).deliveries).toEqual([]);
expect(await closeRuntime(secondPage)).toBe("CLOSED");
expect(await publish(page)).toEqual({
ok: true,
transport: "BROADCAST",
});
await page.waitForTimeout(100);
expect(await snapshot(secondPage)).toEqual({
status: "CLOSED",
deliveries: [
{
ordering: "NEXT",
sequence: 1,
topic: TOPIC,
transport: "BROADCAST",
},
],
pulseRetained: false,
});
} finally {
await Promise.allSettled([
disposeRuntime(page),
disposeRuntime(secondPage),
]);
await secondPage.close();
}
});
test("falls back to native localStorage events and removes the pulse during cleanup", async ({
page,
}) => {
const secondPage = await page.context().newPage();
const cacheEpoch = `browser.fallback.${Date.now()}`;
try {
await Promise.all([page.goto("/"), secondPage.goto("/")]);
await page.evaluate(
(pulseKey) => localStorage.removeItem(pulseKey),
STORAGE_PULSE_KEY,
);
const statuses = await Promise.all([
installRuntime(page, "STORAGE", cacheEpoch),
installRuntime(secondPage, "STORAGE", cacheEpoch),
]);
expect(statuses).toEqual([
"ACTIVE_STORAGE_FALLBACK",
"ACTIVE_STORAGE_FALLBACK",
]);
expect(await publish(page)).toEqual({
ok: true,
transport: "STORAGE",
});
await expect
.poll(async () => (await snapshot(secondPage)).deliveries)
.toEqual([
{
ordering: "NEXT",
sequence: 1,
topic: TOPIC,
transport: "STORAGE",
},
]);
expect(await snapshot(page)).toMatchObject({
deliveries: [],
pulseRetained: false,
});
expect(await snapshot(secondPage)).toMatchObject({
pulseRetained: false,
});
expect(await closeRuntime(secondPage)).toBe("CLOSED");
expect(await publish(page)).toEqual({
ok: true,
transport: "STORAGE",
});
await page.waitForTimeout(100);
expect(await snapshot(secondPage)).toEqual({
status: "CLOSED",
deliveries: [
{
ordering: "NEXT",
sequence: 1,
topic: TOPIC,
transport: "STORAGE",
},
],
pulseRetained: false,
});
} finally {
await Promise.allSettled([
disposeRuntime(page),
disposeRuntime(secondPage),
]);
await secondPage.close();
}
});
@@ -0,0 +1,182 @@
import { expect, test } from "../support/browser/strict-browser-test.ts";
const PNG_1X1 = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
"base64",
);
test("fetches and decodes an allowlisted public immutable CDN image", async ({
page,
}) => {
const cdnRequests: string[] = [];
await page.route("https://images.example.test/**", async (route) => {
cdnRequests.push(route.request().url());
await route.fulfill({
status: 200,
body: PNG_1X1,
headers: {
"access-control-allow-origin": "*",
"cache-control":
"public, max-age=31536000, s-maxage=31536000, immutable",
"content-length": String(PNG_1X1.byteLength),
"content-type": "image/png",
vary: "Accept-Encoding",
},
});
});
await page.goto("/");
const result = await page.evaluate(async () => {
const modulePath =
"/src/adapters/browser-transfer/image-cdn/index.ts";
const {
ImageCdnPolicyRegistry,
createBrowserImageProbe,
createImageCdnRuntime,
imageCdnPresetReference,
} = (await import(
/* @vite-ignore */ modulePath
)) as typeof import("../../src/adapters/browser-transfer/image-cdn/index.ts");
const preset = imageCdnPresetReference(
"browser-probe",
"verify-public-cdn-image",
);
const policies = new ImageCdnPolicyRegistry({
applicationOrigin: "https://app.example.test",
origins: [
{
originKey: "browser-images",
origin: "https://images.example.test",
assetPathPrefix: "/v1/assets/",
minimumPublicMaxAgeSeconds: 31_536_000,
},
],
presets: [
{
reference: preset,
bindingId: "browser-probe-v1",
width: 1,
height: 1,
fit: "cover",
dprs: [1],
responsiveWidths: [1],
quality: 90,
formats: ["png"],
sizes: "1px",
loading: "eager",
decoding: "async",
fetchPriority: "high",
referrerPolicy: "no-referrer",
probeMode: "PRIMARY_REQUIRED",
allowUpscale: false,
maxTransformedPixels: 1,
maxDecodedBytes: 4,
maxEncodedBytes: 128,
},
],
hardLimits: {
maxIntrinsicWidth: 1,
maxIntrinsicHeight: 1,
maxSourcePixels: 1,
maxCssDimension: 1,
maxDpr: 1,
maxQuality: 90,
maxCandidateCount: 1,
maxTransformedPixels: 1,
maxDecodedBytes: 4,
maxEncodedBytes: 128,
maxUrlLength: 2_048,
maxCapabilityLifetimeMs: 60_000,
maxClockSkewMs: 1_000,
minCapabilityRemainingMs: 1_000,
maxPresetBindingsPerCapability: 1,
maxConcurrentCapabilityVerifications: 1,
allowedSourceMediaTypes: ["image/png"],
formatQualityCeilings: { png: 90 },
},
capability: {
issuer: "browser-image-bff",
acceptedKeyIds: ["browser-image-key-v1"],
},
});
const runtime = createImageCdnRuntime({
policies,
probe: createBrowserImageProbe(),
});
const accepted = runtime.assets.acceptPublicImmutable({
kind: "ALLOWLISTED_PUBLIC",
originKey: "browser-images",
assetId: "asset_1x1",
revision: "revision_1",
mediaType: "image/png",
contentKind: "RASTER_STATIC",
intrinsicWidth: 1,
intrinsicHeight: 1,
});
if (!accepted.ok) {
runtime.close();
return {
accepted,
resolved: null,
};
}
const resolved = await runtime.presentation.resolve({
asset: accepted.value,
preset,
signal: new AbortController().signal,
});
runtime.close();
return {
accepted: { ok: true as const },
resolved,
};
});
expect(result.accepted).toEqual({ ok: true });
expect(result.resolved).toMatchObject({
ok: true,
value: {
width: 1,
height: 1,
fallbackMediaType: "image/png",
srcSet: expect.stringMatching(/ 1w$/u),
sources: [],
loading: "eager",
decoding: "async",
fetchPriority: "high",
referrerPolicy: "no-referrer",
crossOrigin: "anonymous",
delivery: {
class: "PUBLIC_IMMUTABLE",
assetVersion: "revision_1",
browserCache: "PUBLIC_IMMUTABLE",
sharedCache: "PUBLIC_IMMUTABLE",
purge: "REVISION_ROLLOVER",
expiresAtEpochMs: null,
},
decodeBudget: {
maximumCandidatePixels: 1,
maximumDecodedBytes: 4,
maximumEncodedBytes: 128,
},
},
});
if (!result.resolved?.ok) return;
const imageUrl = new URL(result.resolved.value.src);
expect(imageUrl.origin).toBe("https://images.example.test");
expect(imageUrl.pathname).toBe(
"/v1/assets/asset_1x1/revision_1",
);
expect(Object.fromEntries(imageUrl.searchParams)).toEqual({
dpr: "1",
fit: "cover",
format: "png",
height: "1",
preset: "browser-probe-v1",
quality: "90",
width: "1",
});
expect(cdnRequests).toEqual([result.resolved.value.src]);
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,221 @@
import { expect, test } from "../support/browser/strict-browser-test.ts";
test("runs put, open, remove and reconcile through a native DedicatedWorker, OPFS and IndexedDB", async ({
page,
}) => {
await page.goto("/");
const result = await page.evaluate(async () => {
const runtimeModulePath =
"/src/adapters/storage/opfs/browser-opfs-runtime.ts";
const policyModulePath =
"/src/adapters/storage/opfs/opfs-policy.ts";
const journalModulePath =
"/src/adapters/storage/opfs/indexeddb-opfs-journal.ts";
const {
createBrowserOpfsRuntime,
inspectBrowserOpfsSupport,
} = (await import(
/* @vite-ignore */ runtimeModulePath
)) as typeof import("../../src/adapters/storage/opfs/browser-opfs-runtime.ts");
const {
DEFAULT_OPFS_RUNTIME_POLICY,
resolveOpfsRuntimePolicy,
} = (await import(
/* @vite-ignore */ policyModulePath
)) as typeof import("../../src/adapters/storage/opfs/opfs-policy.ts");
const { opfsJournalDatabaseName } = (await import(
/* @vite-ignore */ journalModulePath
)) as typeof import("../../src/adapters/storage/opfs/indexeddb-opfs-journal.ts");
const policy = resolveOpfsRuntimePolicy();
const support = inspectBrowserOpfsSupport(policy);
if (!support.ok) {
return {
nativeSupport: false as const,
capabilities: support,
};
}
const suffix = crypto.randomUUID().replaceAll("-", "");
const scope = {
namespace: "durable-objects",
authorityToken: `authority_${suffix}`,
namespaceToken: `namespace_${suffix}`,
partitionToken: `partition_${suffix}`,
} as const;
const storagePolicy = {
owner: "browser-capability",
namespace: scope.namespace,
classification: "PERSONAL",
authority: "LOCAL_FIRST",
accountScope: "OPAQUE_PARTITION",
retention: { kind: "EXPLICIT_DELETE" },
softBudgetBytes: 1024 * 1024,
hardBudgetBytes: 2 * 1024 * 1024,
evictionPriority: "USER_AUTHORED",
logoutAction: "EXPORT_THEN_PURGE",
accountDeletionAction: "PURGE_PARTITION",
pressureAction: "RETAIN",
unavailableFallback: "EXPORT_REQUIRED",
} as const;
const runtime = createBrowserOpfsRuntime({
workerUrl: new URL(
"/tests/browser-capabilities/opfs-test.worker.ts",
location.href,
),
workerName: `opfs-capability-${suffix}`,
scope,
storagePolicy,
policy,
});
const bytes = new Uint8Array([3, 1, 4, 1, 5, 9]);
let capabilities: unknown;
let put: unknown;
let opened:
| Awaited<ReturnType<typeof runtime.objects.open>>
| undefined;
let readBytes: number[] = [];
let removed: unknown;
let afterRemove: unknown;
let reconciled: unknown;
try {
capabilities = await runtime.objects.capabilities();
put = await runtime.objects.put({
objectId: "object_browser_12345678",
expectedGeneration: null,
mediaType: "application/octet-stream",
source: {
byteLength: bytes.byteLength,
async *stream(signal: AbortSignal) {
if (signal.aborted) {
yield {
ok: false as const,
error: {
code: "ABORTED" as const,
operation: "OBJECT_WRITE" as const,
retryable: false,
recovery: "NONE" as const,
},
};
return;
}
yield { ok: true as const, value: Uint8Array.from(bytes) };
},
},
});
opened = await runtime.objects.open({
objectId: "object_browser_12345678",
});
if (
opened &&
typeof opened === "object" &&
"ok" in opened &&
opened.ok
) {
for await (const chunk of opened.value.source.stream(
new AbortController().signal,
)) {
if (!chunk.ok) throw new Error(chunk.error.code);
readBytes.push(...chunk.value);
}
}
removed = await runtime.objects.remove({
objectId: "object_browser_12345678",
expectedGeneration: 1,
});
afterRemove = await runtime.objects.open({
objectId: "object_browser_12345678",
});
reconciled = await runtime.maintenance.reconcile({
budgetMs: 5_000,
maxTransactions: 100,
});
} finally {
runtime.close();
}
const deletion = await new Promise<string>((resolve) => {
const request = indexedDB.deleteDatabase(
opfsJournalDatabaseName(scope.authorityToken),
);
request.onsuccess = () => resolve("DELETED");
request.onerror = () =>
resolve(request.error?.name ?? "UNKNOWN_ERROR");
request.onblocked = () => resolve("BLOCKED");
});
let opfsCleanup = "ABSENT";
try {
const originRoot = await navigator.storage.getDirectory();
const runtimeRoot = await originRoot.getDirectoryHandle(
DEFAULT_OPFS_RUNTIME_POLICY.rootDirectoryName,
);
const authorities = await runtimeRoot.getDirectoryHandle(
"authorities",
);
await authorities.removeEntry(scope.authorityToken, {
recursive: true,
});
opfsCleanup = "DELETED";
} catch (error) {
if (!(error instanceof DOMException) || error.name !== "NotFoundError") {
throw error;
}
}
return {
nativeSupport: true as const,
capabilities,
put,
opened:
opened &&
typeof opened === "object" &&
"ok" in opened &&
opened.ok
? {
ok: true,
generation: opened.value.descriptor.generation,
}
: opened,
readBytes,
removed,
afterRemove,
reconciled,
deletion,
opfsCleanup,
};
});
if (!result.nativeSupport) {
expect(result.capabilities).toMatchObject({
ok: false,
error: { code: "UNSUPPORTED" },
});
return;
}
expect(result.capabilities).toMatchObject({
ok: true,
value: {
available: true,
dedicatedWorkerRequired: true,
crossContextMutationLockAvailable: true,
},
});
expect(result.put).toMatchObject({
ok: true,
value: { generation: 1, byteLength: 6 },
});
expect(result.opened).toEqual({ ok: true, generation: 1 });
expect(result.readBytes).toEqual([3, 1, 4, 1, 5, 9]);
expect(result.removed).toEqual({ ok: true });
expect(result.afterRemove).toMatchObject({
ok: false,
error: { code: "NOT_FOUND" },
});
expect(result.reconciled).toMatchObject({
ok: true,
value: {
inspectedTransactions: 0,
orphanGcStatus: "COMPLETED",
},
});
expect(result.deletion).toBe("DELETED");
expect(["DELETED", "ABSENT"]).toContain(result.opfsCleanup);
});
@@ -0,0 +1,13 @@
import {
startBrowserOpfsDedicatedWorker,
type OpfsWorkerMessageHost,
} from "../../src/adapters/storage/opfs/opfs-worker-runtime.ts";
void startBrowserOpfsDedicatedWorker(
self as unknown as OpfsWorkerMessageHost,
{
storageManager: navigator.storage,
lockManager: navigator.locks,
crypto,
},
);
@@ -0,0 +1,366 @@
import { createHash } from "node:crypto";
import { expect, test } from "../support/browser/strict-browser-test.ts";
const RESOURCE_ID = "browser-artifact-1";
const OBJECT_ORIGIN = "https://objects.example.test";
const OBJECT_PATH = "/files/browser-artifact-1";
const OBJECT_HREF =
`${OBJECT_ORIGIN}${OBJECT_PATH}?sig=opaque-browser-signature`;
const DOWNLOAD_BYTES = Buffer.from([
0x10, 0x20, 0x30, 0x40, 0x50,
0x60, 0x70, 0x80, 0x90, 0xa0,
]);
const DOWNLOAD_SHA256 = createHash("sha256")
.update(DOWNLOAD_BYTES)
.digest("hex");
test("issues an opaque capability and streams a verified object into the writable save path", async ({
page,
}) => {
const bffRequests: unknown[] = [];
const objectRequests: Readonly<{
url: string;
method: string;
accept: string | undefined;
cookie: string | undefined;
}>[] = [];
await page.route("**/api/download-capability", async (route) => {
bffRequests.push(route.request().postDataJSON());
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
capabilityReceipt: "browser-download-capability-1",
method: "GET",
binding: {
kind: "DOWNLOAD",
resourceId: RESOURCE_ID,
},
href: OBJECT_HREF,
origin: OBJECT_ORIGIN,
path: OBJECT_PATH,
allowedQueryParameters: ["sig"],
requestHeaders: [
{
name: "accept",
value: "application/octet-stream",
},
],
requiredResponseHeaders: [
{
name: "x-policy-version",
value: "v1",
},
],
digestRequestHeader: null,
digestResponseHeader: "x-content-sha256",
receiptResponseHeader: null,
expectedStatus: 200,
expectedResponseByteLength: null,
mediaType: "application/octet-stream",
byteLength: DOWNLOAD_BYTES.byteLength,
maxBytes: 64,
expectedSha256: DOWNLOAD_SHA256,
expiresAtEpochMs: Date.now() + 5 * 60_000,
singleUse: true,
}),
});
});
await page.route(`${OBJECT_ORIGIN}/**`, async (route) => {
const headers = route.request().headers();
objectRequests.push({
url: route.request().url(),
method: route.request().method(),
accept: headers.accept,
cookie: headers.cookie,
});
await route.fulfill({
status: 200,
body: DOWNLOAD_BYTES,
headers: {
"access-control-allow-origin": "*",
"access-control-expose-headers":
"content-length, x-content-sha256, x-policy-version",
"content-length": String(DOWNLOAD_BYTES.byteLength),
"content-type": "application/octet-stream",
"x-content-sha256": DOWNLOAD_SHA256,
"x-policy-version": "v1",
},
});
});
await page.goto("/");
const result = await page.evaluate(
async ({ resourceId }) => {
const vaultModulePath =
"/src/adapters/browser-transfer/presigned/presigned-capability-vault.ts";
const providerModulePath =
"/src/adapters/browser-transfer/presigned/presigned-capability-http-provider.ts";
const executorModulePath =
"/src/adapters/browser-transfer/presigned/presigned-transfer-executor.ts";
const policyModulePath =
"/src/adapters/browser-files/browser-file-policy-registry.ts";
const deliveryModulePath =
"/src/adapters/browser-files/download-delivery-adapter.ts";
const [
{
createPresignedCapabilityVault,
createSingleUsePresignedReplayGuard,
},
{ createPresignedCapabilityHttpProvider },
{ createPresignedTransferExecutor },
{
BrowserFilePolicyRegistry,
browserFilePolicyReference,
},
{ createDownloadDeliveryAdapter },
] = await Promise.all([
import(
/* @vite-ignore */ vaultModulePath
) as Promise<
typeof import("../../src/adapters/browser-transfer/presigned/presigned-capability-vault.ts")
>,
import(
/* @vite-ignore */ providerModulePath
) as Promise<
typeof import("../../src/adapters/browser-transfer/presigned/presigned-capability-http-provider.ts")
>,
import(
/* @vite-ignore */ executorModulePath
) as Promise<
typeof import("../../src/adapters/browser-transfer/presigned/presigned-transfer-executor.ts")
>,
import(
/* @vite-ignore */ policyModulePath
) as Promise<
typeof import("../../src/adapters/browser-files/browser-file-policy-registry.ts")
>,
import(
/* @vite-ignore */ deliveryModulePath
) as Promise<
typeof import("../../src/adapters/browser-files/download-delivery-adapter.ts")
>,
]);
const vault = createPresignedCapabilityVault({
maxActiveCapabilities: 4,
});
const provider = createPresignedCapabilityHttpProvider({
endpoint: `${location.origin}/api/download-capability`,
vault,
allowedDataOrigins: ["https://objects.example.test"],
allowedDataPathPrefixes: ["/files/"],
allowedQueryParameters: ["sig"],
allowedRequestHeaders: ["accept"],
allowedResponseHeaders: [
"x-content-sha256",
"x-policy-version",
],
hardMaxTransferBytes: 64,
hardMaxUploadResponseBytes: 1_024,
maxCapabilityTtlMs: 10 * 60_000,
minimumRemainingLifetimeMs: 30_000,
timeoutMs: 5_000,
allowInsecureLocalhost: true,
});
const executor = createPresignedTransferExecutor({
vault,
replayGuard: createSingleUsePresignedReplayGuard(),
hardMaxTransferBytes: 64,
hardMaxChunkBytes: 3,
hardMaxUploadResponseBytes: 1_024,
minimumRemainingLifetimeMs: 30_000,
timeoutMs: 5_000,
});
const signal = new AbortController().signal;
const issued = await provider.issueDownload({
resourceId,
signal,
});
if (!issued.ok) {
vault.dispose();
return {
issued,
delivery: null,
capabilityKeys: [],
capabilityBindingKeys: [],
written: [],
writeChunkSizes: [],
writableClosed: false,
writableAborted: false,
progressPhases: [],
};
}
const downloadPolicy = browserFilePolicyReference(
"browser-presigned-download",
"save-verified-artifact",
);
const policies = new BrowserFilePolicyRegistry({
profiles: [
{
reference: downloadPolicy,
download: {
strategy: "PROMPT_AND_STREAM",
mediaType: "application/octet-stream",
safeExtension: ".bin",
maxTransferBytes: 64,
maxBufferedBytes: 8,
integrity: "REQUIRED",
},
},
],
hardLimits: {
maxInspectionBytes: 64,
maxRetainedFileBytes: 64,
maxPreviewBytes: 64,
maxObjectUrlBytes: 64,
maxTransferBytes: 64,
},
});
const written: number[] = [];
const writeChunkSizes: number[] = [];
let writableClosed = false;
let writableAborted = false;
const downloads = createDownloadDeliveryAdapter({
host: {
handoff() {
throw new TypeError(
"Writable delivery must not use browser handoff.",
);
},
},
policies,
hardMaxObjectUrlBytes: 64,
hardMaxTransferBytes: 64,
browserManagedCapabilities: {
resolve() {
throw new TypeError(
"Browser-managed capability is not used.",
);
},
},
openAuthorizedSource:
executor.downloadSources.open.bind(
executor.downloadSources,
),
showSaveFilePicker: async () => ({
async createWritable() {
return new WritableStream<Uint8Array>({
write(chunk) {
writeChunkSizes.push(chunk.byteLength);
written.push(...chunk);
},
close() {
writableClosed = true;
},
abort() {
writableAborted = true;
},
});
},
}),
createTransferId: () => "transfer:browser-presigned",
progressMinIntervalMs: 0,
userActivation: { isActive: true },
});
const progressPhases: string[] = [];
const capabilityKeys = Object.keys(issued.value).sort();
const capabilityBindingKeys = Object.keys(
issued.value.binding,
).sort();
const delivery = await downloads.deliver({
policy: downloadPolicy,
source: {
kind: "AUTHORIZED_STREAM_RESOURCE",
resourceId,
capability: issued.value,
},
suggestedFileName: "browser-artifact.bin",
signal,
onProgress(progress) {
progressPhases.push(progress.phase);
},
});
downloads.dispose();
vault.dispose();
return {
issued: {
ok: true as const,
exposedHref: "href" in issued.value,
exposedRequestHeaders: "requestHeaders" in issued.value,
},
delivery,
capabilityKeys,
capabilityBindingKeys,
written,
writeChunkSizes,
writableClosed,
writableAborted,
progressPhases,
};
},
{ resourceId: RESOURCE_ID },
);
expect(bffRequests).toEqual([
{
method: "GET",
binding: {
kind: "DOWNLOAD",
resourceId: RESOURCE_ID,
},
},
]);
expect(objectRequests).toEqual([
{
url: OBJECT_HREF,
method: "GET",
accept: "application/octet-stream",
cookie: undefined,
},
]);
expect(result.issued).toEqual({
ok: true,
exposedHref: false,
exposedRequestHeaders: false,
});
expect(result.capabilityKeys).toEqual([
"binding",
"byteLength",
"capabilityReceipt",
"expectedSha256",
"expiresAtEpochMs",
"maxBytes",
"mediaType",
"method",
]);
expect(result.capabilityBindingKeys).toEqual([
"kind",
"resourceId",
]);
expect(result.delivery).toEqual({
ok: true,
value: {
kind: "SAVED",
transferId: "transfer:browser-presigned",
bytesWritten: DOWNLOAD_BYTES.byteLength,
integrity: "VERIFIED",
},
});
expect(result.written).toEqual([...DOWNLOAD_BYTES]);
expect(result.writeChunkSizes).toEqual([3, 3, 3, 1]);
expect(result.writableClosed).toBe(true);
expect(result.writableAborted).toBe(false);
expect(result.progressPhases).toEqual([
"PREPARING",
"TRANSFERRING",
"TRANSFERRING",
"TRANSFERRING",
"TRANSFERRING",
"VERIFYING",
"FINALIZING",
]);
});
@@ -0,0 +1,312 @@
import { expect, test } from "../support/browser/strict-browser-test.ts";
test("keeps A active across a failed B stage, activates B with A rollback, and cleans only owned stale caches", async ({
page,
}) => {
await page.goto("/");
const result = await page.evaluate(async () => {
const adapterModulePath =
"/src/adapters/cache-storage/public-response-cache-adapter.ts";
const policyModulePath =
"/src/adapters/cache-storage/public-cache-policy.ts";
const {
computePublicCacheManifestDigestHex,
createPublicCacheWebLock,
createPublicResponseCacheAdapter,
} = (await import(
/* @vite-ignore */ adapterModulePath
)) as typeof import("../../src/adapters/cache-storage/public-response-cache-adapter.ts");
const { createDefaultPublicCachePolicy } = (await import(
/* @vite-ignore */ policyModulePath
)) as typeof import("../../src/adapters/cache-storage/public-cache-policy.ts");
const suffix = crypto.randomUUID().replaceAll("-", "");
const releaseAId = `browser-a-${suffix.slice(0, 20)}`;
const releaseBId = `browser-b-${suffix.slice(0, 20)}`;
const unrelatedName = `unrelated-${suffix}`;
const policy = {
...createDefaultPublicCachePolicy(location.origin),
ownedCachePrefix: `cap-${suffix.slice(0, 24)}:`,
mutationLockName: `cap-${suffix}:mutation`,
};
const payloads = {
[`${location.origin}/cache-${suffix}-a.js`]: {
bytes: new TextEncoder().encode("verified-release-a"),
contentType: "application/javascript",
},
[`${location.origin}/cache-${suffix}-b.js`]: {
bytes: new TextEncoder().encode("verified-release-b"),
contentType: "application/javascript",
},
[`${location.origin}/cache-${suffix}-b.css`]: {
bytes: new TextEncoder().encode(".release-b{display:block}"),
contentType: "text/css",
},
} as const;
const [aUrl, bUrl, bSecondUrl] = Object.keys(payloads);
const digestHex = async (bytes: Uint8Array): Promise<string> => {
const digest = await crypto.subtle.digest(
"SHA-256",
Uint8Array.from(bytes),
);
return Array.from(
new Uint8Array(digest),
(byte) => byte.toString(16).padStart(2, "0"),
).join("");
};
const asset = async (absoluteUrl: string) => {
const payload = payloads[absoluteUrl as keyof typeof payloads];
return {
absoluteUrl,
expectedByteLength: payload.bytes.byteLength,
expectedContentType: payload.contentType,
integrity: {
algorithm: "SHA-256" as const,
digestHex: await digestHex(payload.bytes),
},
};
};
const assetsA = [await asset(aUrl!)];
const assetsB = [
await asset(bUrl!),
await asset(bSecondUrl!),
];
const manifestA = {
releaseRegistryId: releaseAId,
assets: assetsA,
manifestDigestHex:
await computePublicCacheManifestDigestHex(
crypto,
releaseAId,
assetsA,
policy,
),
};
const manifestB = {
releaseRegistryId: releaseBId,
assets: assetsB,
manifestDigestHex:
await computePublicCacheManifestDigestHex(
crypto,
releaseBId,
assetsB,
policy,
),
};
let corruptSecondBAsset = true;
const adapter = createPublicResponseCacheAdapter({
cacheStorage: caches,
crypto,
mutationLock: createPublicCacheWebLock(
navigator.locks,
policy.mutationLockName,
),
policy,
fetcher: async (request) => {
if (request.signal.aborted) {
throw new DOMException("Aborted.", "AbortError");
}
const payload =
payloads[request.url as keyof typeof payloads];
if (!payload) throw new TypeError("Unexpected cache URL.");
const responseBytes =
corruptSecondBAsset && request.url === bSecondUrl
? new Uint8Array(payload.bytes.byteLength)
: Uint8Array.from(payload.bytes);
return new Response(responseBytes, {
status: 200,
headers: {
"cache-control":
"public, max-age=31536000, immutable",
"content-type": payload.contentType,
"x-account-id": "must-not-persist",
},
});
},
});
const readActive = async (absoluteUrl: string) => {
const matched = await adapter.responses.matchActiveExact({
absoluteUrl,
});
if (!matched.ok || !matched.value) return matched;
const streamed: number[] = [];
for await (const chunk of matched.value.body.stream(
new AbortController().signal,
)) {
if (!chunk.ok) return chunk;
streamed.push(...chunk.value);
}
return {
ok: true as const,
streamed,
headerNames: matched.value.headers.map(([name]) => name),
};
};
try {
const stageA = await adapter.admin.stageRelease(manifestA);
const activateA = await adapter.admin.activateRelease(
manifestA.releaseRegistryId,
manifestA.manifestDigestHex,
);
const matchedABeforeFailedB = await readActive(aUrl!);
const failedStageB =
await adapter.admin.stageRelease(manifestB);
const afterFailedB = await adapter.admin.inspect();
const matchedAAfterFailedB = await readActive(aUrl!);
corruptSecondBAsset = false;
const stageB = await adapter.admin.stageRelease(manifestB);
const activateB = await adapter.admin.activateRelease(
manifestB.releaseRegistryId,
manifestB.manifestDigestHex,
);
const matchedB = await readActive(bUrl!);
const afterActivateB = await adapter.admin.inspect();
const releaseBCacheName = (await caches.keys()).find((name) =>
name.startsWith(
`${policy.ownedCachePrefix}release:${releaseBId}:`,
),
);
if (!releaseBCacheName) {
throw new Error("B release cache not found.");
}
const releaseBCache = await caches.open(releaseBCacheName);
await releaseBCache.put(
new Request(bUrl!),
new Response(new Uint8Array([0]), {
status: 200,
headers: {
"cache-control": "public",
"content-type": "application/javascript",
},
}),
);
const poisoned = await adapter.responses.matchActiveExact({
absoluteUrl: bUrl!,
});
await caches.open(`${policy.ownedCachePrefix}stale`);
await caches.open(unrelatedName);
const cleanup = await adapter.admin.cleanupOwned();
const names = await caches.keys();
return {
stageA,
activateA,
matchedABeforeFailedB,
failedStageB,
afterFailedB,
matchedAAfterFailedB,
stageB,
activateB,
matchedB,
afterActivateB,
poisoned,
cleanup,
unrelatedPreserved: names.includes(unrelatedName),
activeBPreserved: names.some((name) =>
name.startsWith(
`${policy.ownedCachePrefix}release:${releaseBId}:`,
),
),
rollbackAPreserved: names.some((name) =>
name.startsWith(
`${policy.ownedCachePrefix}release:${releaseAId}:`,
),
),
remainingOwnedCount: names.filter((name) =>
name.startsWith(policy.ownedCachePrefix),
).length,
expectedABytes: [...payloads[aUrl! as keyof typeof payloads].bytes],
expectedBBytes: [...payloads[bUrl! as keyof typeof payloads].bytes],
};
} finally {
for (const name of await caches.keys()) {
if (
name.startsWith(policy.ownedCachePrefix) ||
name === unrelatedName
) {
await caches.delete(name);
}
}
}
});
expect(result.stageA).toMatchObject({
ok: true,
value: { entryCount: 1 },
});
expect(result.activateA).toMatchObject({ ok: true });
expect(result.matchedABeforeFailedB).toMatchObject({
ok: true,
streamed: result.expectedABytes,
});
expect(result.failedStageB).toMatchObject({
ok: false,
error: { code: "INTEGRITY_FAILED" },
});
expect(result.afterFailedB).toMatchObject({
ok: true,
value: {
activeReleaseRegistryId: expect.stringMatching(/^browser-a-/u),
releaseCandidates: [
expect.objectContaining({
releaseRegistryId: expect.stringMatching(/^browser-a-/u),
verified: true,
}),
],
},
});
expect(result.matchedAAfterFailedB).toMatchObject({
ok: true,
streamed: result.expectedABytes,
});
expect(result.stageB).toMatchObject({
ok: true,
value: { entryCount: 2 },
});
expect(result.activateB).toMatchObject({ ok: true });
expect(result.matchedB).toMatchObject({
ok: true,
streamed: result.expectedBBytes,
});
expect(
(
result.matchedB as Readonly<{
headerNames: readonly string[];
}>
).headerNames,
).not.toContain("x-account-id");
expect(result.afterActivateB).toMatchObject({
ok: true,
value: {
activeReleaseRegistryId: expect.stringMatching(/^browser-b-/u),
releaseCandidates: expect.arrayContaining([
expect.objectContaining({
releaseRegistryId: expect.stringMatching(/^browser-a-/u),
verified: true,
}),
expect.objectContaining({
releaseRegistryId: expect.stringMatching(/^browser-b-/u),
verified: true,
}),
]),
},
});
expect(result.poisoned).toMatchObject({
ok: false,
error: { code: "INTEGRITY_FAILED" },
});
expect(result.cleanup).toMatchObject({
ok: true,
value: { deletedOwnedCaches: 1, retainedOwnedCaches: 3 },
});
expect(result.unrelatedPreserved).toBe(true);
expect(result.activeBPreserved).toBe(true);
expect(result.rollbackAPreserved).toBe(true);
expect(result.remainingOwnedCount).toBe(3);
});
@@ -0,0 +1,519 @@
import { createHash } from "node:crypto";
import type { Route } from "@playwright/test";
import { expect, test } from "../support/browser/strict-browser-test.ts";
const PAGE_ORIGIN = "http://127.0.0.1:4174";
const API_ORIGIN = "https://upload-api.example.test";
const OBJECT_ORIGIN = "https://upload-objects.example.test";
const SESSION_ID = "session_browser_01";
const RESOURCE_ID = "resource_browser_01";
const UPLOAD_PROTOCOL = "PRESIGNED_MULTIPART_V1";
type JsonRecord = Readonly<Record<string, unknown>>;
function corsHeaders(): Record<string, string> {
return {
"access-control-allow-credentials": "true",
"access-control-allow-headers": "content-type",
"access-control-allow-methods": "POST, OPTIONS",
"access-control-allow-origin": PAGE_ORIGIN,
};
}
async function fulfillJson(
route: Route,
status: number,
value: unknown,
): Promise<void> {
const body = JSON.stringify(value);
await route.fulfill({
status,
body,
headers: {
...corsHeaders(),
"content-length": String(Buffer.byteLength(body)),
"content-type": "application/json; charset=utf-8",
},
});
}
test("uploads three presigned parts through native IndexedDB, Web Locks and fetch", async ({
page,
}) => {
let session: JsonRecord | null = null;
const acceptedParts = new Map<number, JsonRecord>();
const issuedParts = new Map<number, JsonRecord>();
const controlRequests: Readonly<{
path: string;
body: JsonRecord;
}>[] = [];
const uploadedBodies: Readonly<{
partNumber: number;
bytes: number[];
checksum: string | undefined;
}>[] = [];
let completedParts: readonly JsonRecord[] = [];
await page.route(`${API_ORIGIN}/**`, async (route) => {
if (route.request().method() === "OPTIONS") {
await route.fulfill({ status: 204, headers: corsHeaders() });
return;
}
const path = new URL(route.request().url()).pathname;
const body = route.request().postDataJSON() as JsonRecord;
controlRequests.push({ path, body });
if (path === "/uploads/create") {
const fingerprint = body.fingerprint as JsonRecord;
session = {
protocol: UPLOAD_PROTOCOL,
sessionId: SESSION_ID,
requestBindingSha256: body.requestBindingSha256,
fingerprint,
partSizeBytes: body.requestedPartSizeBytes,
partCount: fingerprint.partCount,
maxConcurrency: 1,
expiresAtEpochMs: Date.now() + 5 * 60_000,
};
await fulfillJson(route, 201, session);
return;
}
if (path === "/uploads/status") {
if (!session) {
await fulfillJson(route, 404, { state: "NOT_FOUND" });
return;
}
await fulfillJson(route, 200, {
state: "ACTIVE",
session,
acceptedParts: [...acceptedParts.values()].sort(
(left, right) =>
Number(left.partNumber) - Number(right.partNumber),
),
});
return;
}
if (path === "/uploads/complete") {
completedParts = body.orderedParts as readonly JsonRecord[];
await fulfillJson(route, 200, {
state: "QUARANTINED",
protocol: UPLOAD_PROTOCOL,
sessionId: body.sessionId,
requestBindingSha256: body.requestBindingSha256,
fingerprint: body.fingerprint,
resourceId: RESOURCE_ID,
});
return;
}
if (path === "/uploads/abort") {
await fulfillJson(route, 200, { state: "ABORTED" });
return;
}
if (path === "/uploads/part-capability") {
const binding = body.binding as JsonRecord;
const partNumber = Number(binding.partNumber);
const descriptor = {
partNumber,
offset: binding.offset,
byteLength: body.byteLength,
checksumSha256: body.expectedSha256,
};
issuedParts.set(partNumber, descriptor);
const objectPath =
`/uploads/${SESSION_ID}/parts/${String(partNumber)}`;
await fulfillJson(route, 200, {
capabilityReceipt: `browser-part-capability-${String(partNumber)}`,
method: "PUT",
binding,
href:
`${OBJECT_ORIGIN}${objectPath}?sig=opaque-${String(partNumber)}`,
origin: OBJECT_ORIGIN,
path: objectPath,
allowedQueryParameters: ["sig"],
requestHeaders: [
{
name: "content-type",
value: body.mediaType,
},
{
name: "x-checksum-sha256",
value: body.expectedSha256,
},
],
requiredResponseHeaders: [
{
name: "x-policy-version",
value: "v1",
},
],
digestRequestHeader: "x-checksum-sha256",
digestResponseHeader: null,
receiptResponseHeader: "etag",
expectedResponseByteLength: 0,
expectedStatus: 204,
mediaType: body.mediaType,
byteLength: body.byteLength,
maxBytes: body.byteLength,
expectedSha256: body.expectedSha256,
expiresAtEpochMs:
Number(session?.expiresAtEpochMs) - 10_000,
singleUse: true,
});
return;
}
await route.fulfill({ status: 404, headers: corsHeaders() });
});
await page.route(`${OBJECT_ORIGIN}/**`, async (route) => {
if (route.request().method() === "OPTIONS") {
await route.fulfill({
status: 204,
headers: {
"access-control-allow-headers":
"content-type, x-checksum-sha256",
"access-control-allow-methods": "PUT, OPTIONS",
"access-control-allow-origin": "*",
},
});
return;
}
const match = new URL(route.request().url()).pathname.match(
/\/parts\/([1-9][0-9]*)$/u,
);
const partNumber = Number(match?.[1]);
const body = route.request().postDataBuffer();
if (!body || !Number.isSafeInteger(partNumber)) {
await route.fulfill({ status: 400 });
return;
}
const checksum = route.request().headers()["x-checksum-sha256"];
const actualChecksum = createHash("sha256")
.update(body)
.digest("hex");
const descriptor = issuedParts.get(partNumber);
if (
!descriptor ||
checksum !== actualChecksum ||
descriptor.checksumSha256 !== actualChecksum ||
descriptor.byteLength !== body.byteLength
) {
await route.fulfill({ status: 422 });
return;
}
const receiptToken = `etag-part-${String(partNumber)}`;
acceptedParts.set(partNumber, {
...descriptor,
receiptToken,
});
uploadedBodies.push({
partNumber,
bytes: [...body],
checksum,
});
await route.fulfill({
status: 204,
headers: {
"access-control-allow-origin": "*",
"access-control-expose-headers": "etag, x-policy-version",
etag: `"${receiptToken}"`,
"x-policy-version": "v1",
},
});
});
await page.goto("/");
const result = await page.evaluate(
async ({ apiOrigin, objectOrigin }) => {
const vaultModulePath =
"/src/adapters/browser-transfer/presigned/presigned-capability-vault.ts";
const providerModulePath =
"/src/adapters/browser-transfer/presigned/presigned-capability-http-provider.ts";
const executorModulePath =
"/src/adapters/browser-transfer/presigned/presigned-transfer-executor.ts";
const fetchTransportModulePath =
"/src/adapters/browser-transfer/resumable-upload/fetch-json-transport.ts";
const controlPlaneModulePath =
"/src/adapters/browser-transfer/resumable-upload/http-control-plane-adapter.ts";
const checkpointModulePath =
"/src/adapters/browser-transfer/resumable-upload/indexeddb-checkpoint-store.ts";
const partExecutorModulePath =
"/src/adapters/browser-transfer/resumable-upload/presigned-upload-part-executor.ts";
const uploadRuntimeModulePath =
"/src/adapters/browser-transfer/resumable-upload/resumable-upload-runtime.ts";
const cancellationModulePath =
"/src/adapters/browser-transfer/resumable-upload/upload-cancellation-channel.ts";
const lockModulePath =
"/src/adapters/browser-transfer/resumable-upload/upload-mutation-lock.ts";
const [
{
createPresignedCapabilityVault,
createSingleUsePresignedReplayGuard,
},
{ createPresignedCapabilityHttpProvider },
{ createPresignedTransferExecutor },
{ createResumableUploadFetchJsonTransport },
{ createResumableUploadHttpControlPlane },
{ createIndexedDbResumableUploadCheckpointRuntime },
{ createPresignedUploadPartExecutor },
{ createResumableUploadRuntime },
{ createBrowserUploadCancellationChannel },
{ createResumableUploadWebLock },
] = await Promise.all([
import(/* @vite-ignore */ vaultModulePath),
import(/* @vite-ignore */ providerModulePath),
import(/* @vite-ignore */ executorModulePath),
import(/* @vite-ignore */ fetchTransportModulePath),
import(/* @vite-ignore */ controlPlaneModulePath),
import(/* @vite-ignore */ checkpointModulePath),
import(/* @vite-ignore */ partExecutorModulePath),
import(/* @vite-ignore */ uploadRuntimeModulePath),
import(/* @vite-ignore */ cancellationModulePath),
import(/* @vite-ignore */ lockModulePath),
]);
const suffix = crypto.randomUUID().replaceAll("-", "");
const uploadKey = `upload_browser_${suffix}`;
const checkpointRuntime =
createIndexedDbResumableUploadCheckpointRuntime({
scope: {
authorityToken: `authority_${suffix}`,
namespaceToken: `namespace_${suffix}`,
partitionToken: `partition_${suffix}`,
},
blockedTimeoutMs: 2_000,
});
const vault = createPresignedCapabilityVault({
maxActiveCapabilities: 8,
});
const capabilityProvider =
createPresignedCapabilityHttpProvider({
endpoint: `${apiOrigin}/uploads/part-capability`,
vault,
allowedDataOrigins: [objectOrigin],
allowedDataPathPrefixes: ["/uploads/"],
allowedQueryParameters: ["sig"],
allowedRequestHeaders: [
"content-type",
"x-checksum-sha256",
],
allowedResponseHeaders: ["etag", "x-policy-version"],
hardMaxTransferBytes: 8,
hardMaxUploadResponseBytes: 1_024,
maxCapabilityTtlMs: 10 * 60_000,
minimumRemainingLifetimeMs: 30_000,
timeoutMs: 5_000,
controlPlaneCredentials: "include",
});
const transferExecutor = createPresignedTransferExecutor({
vault,
replayGuard: createSingleUsePresignedReplayGuard(),
hardMaxTransferBytes: 8,
hardMaxChunkBytes: 2,
hardMaxUploadResponseBytes: 1_024,
minimumRemainingLifetimeMs: 30_000,
timeoutMs: 5_000,
});
const transport = createResumableUploadFetchJsonTransport({
endpoints: {
CREATE_SESSION: `${apiOrigin}/uploads/create`,
GET_STATUS: `${apiOrigin}/uploads/status`,
COMPLETE: `${apiOrigin}/uploads/complete`,
ABORT: `${apiOrigin}/uploads/abort`,
},
allowedOrigins: [apiOrigin],
credentials: "include",
timeoutMs: 5_000,
maxRequestBytes: 64 * 1024,
maxResponseBytes: 64 * 1024,
maxRetryAfterMs: 1_000,
});
const controlPlane = createResumableUploadHttpControlPlane({
transport,
partCapabilities: capabilityProvider,
});
const cancellationChannelName =
`browser-upload-cancel-${suffix}`;
const crossContextCancellation =
createBrowserUploadCancellationChannel({
channelName: cancellationChannelName,
});
const cancellationPeer =
createBrowserUploadCancellationChannel({
channelName: cancellationChannelName,
});
const runtime = createResumableUploadRuntime({
controlPlane,
partExecutor: createPresignedUploadPartExecutor(
transferExecutor.uploadParts,
),
checkpoints: checkpointRuntime.store,
mutationLock: createResumableUploadWebLock(
navigator.locks,
"browser-resumable-upload",
),
...(crossContextCancellation
? { crossContextCancellation }
: {}),
crypto,
policy: {
partSizeBytes: 2,
maxFileBytes: 6,
maxPartCount: 3,
maxConcurrency: 2,
maxInFlightBytes: 16,
partBufferCopyFactor: 4,
maxSourceChunkBytes: 2,
maxRetries: 1,
retryBaseDelayMs: 1,
retryMaxDelayMs: 5,
maxRetryAfterMs: 1_000,
capabilityRefreshSkewMs: 1_000,
maxSessionLifetimeMs: 10 * 60_000,
providerAttemptTimeoutMs: 10_000,
},
});
const bytes = new Uint8Array([1, 2, 3, 4, 5]);
const progress: string[] = [];
let upload: unknown;
let checkpoint: unknown;
let deletion: unknown;
let cancellationDelivered = false;
try {
if (crossContextCancellation && cancellationPeer) {
cancellationDelivered = await new Promise<boolean>(
(resolve) => {
const timer = setTimeout(() => resolve(false), 2_000);
const release = cancellationPeer.subscribe((key: string) => {
if (key !== `upload_probe_${suffix}`) return;
clearTimeout(timer);
release();
resolve(true);
});
if (
!crossContextCancellation.publish(
`upload_probe_${suffix}`,
)
) {
clearTimeout(timer);
release();
resolve(false);
}
},
);
}
upload = await runtime.upload({
uploadKey,
purpose: "browser_attachment",
mediaType: "application/octet-stream",
source: {
kind: "RANGE_READER",
reader: {
byteLength: bytes.byteLength,
async readRange(input: {
offset: number;
length: number;
signal: AbortSignal;
}) {
return input.signal.aborted
? {
ok: false as const,
error: {
code: "ABORTED" as const,
operation: "FILE_READ" as const,
retryable: false,
recovery: "NONE" as const,
},
}
: {
ok: true as const,
value: bytes.slice(
input.offset,
input.offset + input.length,
),
};
},
},
},
signal: new AbortController().signal,
onProgress(value: { phase: string }) {
progress.push(value.phase);
},
});
checkpoint = await checkpointRuntime.store.read(uploadKey);
} finally {
runtime.close();
cancellationPeer?.close();
deletion =
await checkpointRuntime.admin.deletePartition();
vault.dispose();
}
return {
upload,
checkpoint,
deletion,
progress,
webLocksAvailable: Boolean(navigator.locks),
broadcastCancellationAvailable:
Boolean(crossContextCancellation),
cancellationDelivered,
};
},
{ apiOrigin: API_ORIGIN, objectOrigin: OBJECT_ORIGIN },
);
expect(result.webLocksAvailable).toBe(true);
expect(result.broadcastCancellationAvailable).toBe(true);
expect(result.cancellationDelivered).toBe(true);
expect(result.upload).toMatchObject({
ok: true,
value: {
state: "QUARANTINED",
resourceId: RESOURCE_ID,
byteLength: 5,
replayed: false,
},
});
expect(result.checkpoint).toEqual({ ok: true, value: null });
expect(result.deletion).toEqual({
ok: true,
value: { state: "DELETED" },
});
expect(
uploadedBodies
.slice()
.sort((left, right) => left.partNumber - right.partNumber)
.map(({ partNumber, bytes }) => ({ partNumber, bytes })),
).toEqual([
{ partNumber: 1, bytes: [1, 2] },
{ partNumber: 2, bytes: [3, 4] },
{ partNumber: 3, bytes: [5] },
]);
expect(completedParts.map((part) => part.partNumber)).toEqual([
1, 2, 3,
]);
expect(completedParts.map((part) => part.receiptToken)).toEqual([
"etag-part-1",
"etag-part-2",
"etag-part-3",
]);
expect(controlRequests.map((request) => request.path)).toEqual([
"/uploads/create",
"/uploads/status",
"/uploads/part-capability",
"/uploads/part-capability",
"/uploads/part-capability",
"/uploads/status",
"/uploads/complete",
]);
expect(JSON.stringify(controlRequests)).not.toContain("?sig=");
expect(JSON.stringify(controlRequests)).not.toContain(OBJECT_ORIGIN);
});
@@ -0,0 +1,29 @@
import { expect, test } from "../support/browser/strict-browser-test.ts";
test("executes the storage durability adapter against the real browser StorageManager", async ({
page,
}) => {
await page.goto("/");
const result = await page.evaluate(async () => {
const modulePath =
"/src/adapters/browser-file-storage/storage-manager-adapter.ts";
const { createStorageDurabilityAdapter } = await import(
/* @vite-ignore */ modulePath
);
const adapter = createStorageDurabilityAdapter(navigator.storage);
return adapter.inspect();
});
expect(result.ok).toBe(true);
if (result.ok) {
expect(["UNKNOWN", "NORMAL", "PRESSURE", "CRITICAL"]).toContain(
result.value.pressure,
);
expect(
result.value.usageBytes === null || result.value.usageBytes >= 0,
).toBe(true);
expect(
result.value.quotaBytes === null || result.value.quotaBytes >= 0,
).toBe(true);
}
});
File diff suppressed because it is too large Load Diff
+197
View File
@@ -0,0 +1,197 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { deriveAsyncState } from "../../src/application/view-models/async-state.ts";
import { AsyncSurface } from "../../src/presentation/components/async-surface.tsx";
import { createFailure } from "../../src/contracts/errors.ts";
describe("async UI state matrix", () => {
it.each([
[{ isInitialLoading: true }, "initial-loading"],
[{ data: [{ id: "1" }] }, "success"],
[{ data: [] }, "empty"],
[
{ failure: createFailure("SERVER_FAILURE", "LIST", 0) },
"terminal-error",
],
])("derives base state %#", (signals, expected) => {
expect(deriveAsyncState(signals).base).toBe(expected);
});
it.each([
[{ data: ["value"], isFetching: true }, "refreshing"],
[{ data: ["value"], isStale: true, isDegraded: true }, "stale-degraded"],
[{ data: ["value"], isMutationPending: true }, "mutation-pending"],
[
{ data: ["value"], hasMutationEffectUnknown: true },
"mutation-effect-unknown",
],
[{ data: ["value"], hasMutationConflict: true }, "mutation-conflict"],
])("derives overlay state %#", (signals, indicator) => {
expect(deriveAsyncState(signals).indicator).toBe(indicator);
});
it("makes crossed overlay inputs mutually exclusive by priority", () => {
const state = deriveAsyncState({
data: ["value"],
isFetching: true,
isMutationPending: true,
hasMutationEffectUnknown: true,
hasMutationConflict: true,
});
expect(state.indicator).toBe("mutation-effect-unknown");
expect(state.overlay).toMatchObject({
refreshing: false,
mutationPending: false,
mutationEffectUnknown: true,
mutationConflict: false,
});
});
it("renders unknown mutation effects with reconciliation-only actions", async () => {
const user = userEvent.setup();
const retry = vi.fn();
const reconcile = vi.fn();
const state = deriveAsyncState({
data: ["value"],
hasMutationEffectUnknown: true,
});
render(
<AsyncSurface
state={state}
onRetry={retry}
onReconcileUnknownEffect={reconcile}
>
existing content
</AsyncSurface>,
);
expect(screen.getByRole("status")).toHaveTextContent(
"변경 결과를 확인할 수 없습니다.",
);
expect(
screen.getByText("existing content").closest("section"),
).toHaveAttribute("aria-busy", "false");
expect(
screen.queryByRole("button", { name: "다시 시도" }),
).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "변경됨으로 확인" }));
await user.click(
screen.getByRole("button", { name: "변경되지 않음으로 확인" }),
);
expect(reconcile).toHaveBeenNthCalledWith(1, "APPLIED");
expect(reconcile).toHaveBeenNthCalledWith(2, "NOT_APPLIED");
expect(retry).not.toHaveBeenCalled();
});
it("keeps content visible while a non-blocking refresh runs", () => {
const state = deriveAsyncState({ data: ["value"], isFetching: true });
render(<AsyncSurface state={state}>existing content</AsyncSurface>);
expect(screen.getByText("existing content")).toBeVisible();
expect(screen.getByRole("status")).toHaveTextContent(
"최신 정보를 확인하고 있습니다.",
);
});
it("connects stale retry and conflict resolution to real callbacks", async () => {
const user = userEvent.setup();
const retry = vi.fn();
const resolveConflict = vi.fn();
const stale = deriveAsyncState({
data: ["value"],
isStale: true,
isDegraded: true,
});
const view = render(
<AsyncSurface state={stale} onRetry={retry}>
existing content
</AsyncSurface>,
);
await user.click(screen.getByRole("button", { name: "다시 시도" }));
expect(retry).toHaveBeenCalledOnce();
const conflict = deriveAsyncState({
data: ["value"],
hasMutationConflict: true,
});
view.rerender(
<AsyncSurface
state={conflict}
onResolveConflict={resolveConflict}
>
existing content
</AsyncSurface>,
);
await user.click(screen.getByRole("button", { name: "충돌 해결" }));
expect(resolveConflict).toHaveBeenCalledOnce();
});
it("renders only safe error vocabulary", () => {
const failure = createFailure("SERVER_FAILURE", "LIST", 0, {
code: "SERVER_FAILURE",
});
const state = deriveAsyncState({ failure });
render(<AsyncSurface state={state} onRetry={vi.fn()} />);
expect(screen.getByRole("alert")).toHaveTextContent(
"요청을 완료하지 못했습니다.",
);
expect(screen.getByRole("alert")).toHaveAttribute(
"data-message-key",
failure.userMessageKey,
);
expect(screen.getByRole("button")).toHaveTextContent("다시 시도");
expect(screen.getByRole("alert")).not.toHaveTextContent("stack");
});
it("routes terminal actions by failure semantics", async () => {
const user = userEvent.setup();
const retry = vi.fn();
const action = vi.fn();
const forbidden = deriveAsyncState({
failure: createFailure("FORBIDDEN", "LIST", 0),
});
const view = render(
<AsyncSurface
state={forbidden}
onAction={action}
onRetry={retry}
/>,
);
await user.click(
screen.getByRole("button", { name: "안전한 화면으로 이동" }),
);
expect(action).toHaveBeenCalledOnce();
expect(retry).not.toHaveBeenCalled();
const retryable = deriveAsyncState({
failure: createFailure("SERVER_FAILURE", "LIST", 0),
});
view.rerender(
<AsyncSurface
state={retryable}
onAction={action}
onRetry={retry}
/>,
);
await user.click(screen.getByRole("button", { name: "다시 시도" }));
expect(retry).toHaveBeenCalledOnce();
expect(action).toHaveBeenCalledOnce();
});
it("does not retain a terminal error after usable data is restored", () => {
const failed = deriveAsyncState({
failure: createFailure("SERVER_FAILURE", "LIST", 0),
});
const recovered = deriveAsyncState({ data: ["value"] });
expect(failed.base).toBe("terminal-error");
expect(recovered.base).toBe("success");
expect(recovered.failure).toBeUndefined();
});
});
+17
View File
@@ -0,0 +1,17 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
function TestShell() {
return <main aria-label="application shell">ready</main>;
}
describe("component test level", () => {
it("renders an accessible application shell", () => {
render(<TestShell />);
expect(screen.getByRole("main", { name: "application shell" })).toHaveTextContent(
"ready",
);
});
});
+54
View File
@@ -0,0 +1,54 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { SafeText } from "../../src/presentation/security/safe-text.tsx";
import { assertSafeConfigNames } from "../../src/contracts/env.ts";
import { defineStorageKey } from "../../src/contracts/storage-keys.ts";
import { projectTelemetryEvent } from "../../src/contracts/telemetry.ts";
describe("browser security boundary", () => {
it("renders untrusted text without script or inline handler injection", () => {
render(
<SafeText value={'<img src=x onerror="window.compromised=true"><script>x</script>'} />,
);
expect(screen.getByText(/<img/)).toBeVisible();
expect(document.querySelector("script")).toBeNull();
expect(document.querySelector("[onerror]")).toBeNull();
});
it("rejects secret-like client configuration names", () => {
expect(() => assertSafeConfigNames({ PRIVATE_KEY: "not-public" })).toThrow();
});
it("rejects browser token storage registration", () => {
expect(() =>
defineStorageKey({
logicalName: "SESSION_TOKEN",
scope: "auth",
name: "session-token",
backend: "sessionStorage",
classification: "sensitive-forbidden",
schemaVersion: 1,
valueCodec: "none",
ttl: "session",
migration: "discard",
quotaFallback: "feature-disable",
}),
).toThrow();
});
it("drops raw URL/query/token telemetry attributes", () => {
const result = projectTelemetryEvent("api.request.failed", {
error_kind: "SERVER_FAILURE",
http_status_group: "5xx",
attempt_count_bucket: "1",
route_id: "APP_HOME",
raw_url: "https://api.test?token=private",
query_string: "token=private",
});
expect(result.success).toBe(true);
expect(JSON.stringify(result)).not.toMatch(/raw_url|query_string|private/);
});
});
@@ -0,0 +1,68 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import {
ChunkRecoveryBoundary,
isChunkLoadFailure,
} from "../../src/presentation/boundaries/chunk-recovery-boundary.tsx";
import { FeatureBoundary } from "../../src/presentation/boundaries/render-error-boundary.tsx";
function ChunkDefect(): never {
throw new TypeError("Failed to fetch dynamically imported module");
}
function RenderDefect(): never {
throw new Error("ordinary render defect");
}
describe("chunk recovery boundary classification", () => {
it("recognizes lazy module failures without classifying ordinary render errors", () => {
expect(
isChunkLoadFailure(
new TypeError("Failed to fetch dynamically imported module"),
),
).toBe(true);
expect(isChunkLoadFailure(new Error("ordinary render defect"))).toBe(false);
});
it("runs the recovery input only for a lazy chunk rejection", async () => {
const recover = vi.fn(async () => ({
action: "support" as const,
reason: "reload-already-attempted",
}));
render(
<ChunkRecoveryBoundary chunkId="route-home" recover={recover}>
<ChunkDefect />
</ChunkRecoveryBoundary>,
);
expect(
await screen.findByRole("heading", {
name: "화면 자산을 복구하지 못했습니다.",
}),
).toBeVisible();
expect(recover).toHaveBeenCalledOnce();
expect(recover).toHaveBeenCalledWith({
chunkId: "route-home",
failureKind: "CHUNK_LOAD_FAILURE",
});
});
it("rethrows an ordinary component defect to the local render boundary", () => {
const recover = vi.fn();
render(
<FeatureBoundary routeId="APP_HOME" buildId="build-a">
<ChunkRecoveryBoundary chunkId="route-home" recover={recover}>
<RenderDefect />
</ChunkRecoveryBoundary>
</FeatureBoundary>,
);
expect(screen.getByRole("alert")).toHaveTextContent(
"화면을 표시하지 못했습니다.",
);
expect(recover).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,269 @@
// @vitest-environment jsdom
import {
act,
fireEvent,
render,
screen,
waitFor,
within,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { describe, expect, it, vi } from "vitest";
import {
Button,
Checkbox,
DataTable,
Drawer,
IconButton,
Menu,
MenuIcon,
Pagination,
Popover,
RadioGroup,
Switch,
Tabs,
ToastProvider,
useToast,
} from "../../src/presentation/design-system/index.ts";
describe("design-system platform interactions", () => {
it("keeps decorative icons out of the accessibility tree and names icon actions", () => {
render(
<>
<MenuIcon />
<IconButton accessibleName="탐색 열기">
<MenuIcon />
</IconButton>
</>,
);
expect(screen.getByRole("button", { name: "탐색 열기" })).toBeVisible();
expect(screen.queryByRole("img")).not.toBeInTheDocument();
});
it("supports native choices, indeterminate state, radio arrows and switches", async () => {
const user = userEvent.setup();
const onRadio = vi.fn();
const onSwitch = vi.fn();
render(
<>
<Checkbox indeterminate label="일부 선택" />
<RadioGroup
label="밀도"
name="density"
onChange={onRadio}
options={[
{ value: "normal", label: "보통" },
{ value: "compact", label: "조밀" },
]}
value="normal"
/>
<Switch
checked={false}
label="알림"
onChange={onSwitch}
/>
</>,
);
const checkbox = screen.getByRole("checkbox", { name: "일부 선택" });
expect(checkbox).toBePartiallyChecked();
const normal = screen.getByRole("radio", { name: "보통" });
normal.focus();
await user.keyboard("{ArrowRight}");
expect(onRadio).toHaveBeenCalledWith("compact");
await user.click(screen.getByRole("switch", { name: "알림" }));
expect(onSwitch).toHaveBeenCalledWith(true);
});
it("moves through a menu, executes once, dismisses and restores focus", async () => {
const user = userEvent.setup();
const inspect = vi.fn();
render(
<Menu
items={[
{ id: "open", label: "열기", onSelect: vi.fn() },
{ id: "inspect", label: "검사", onSelect: inspect },
]}
triggerLabel="작업"
/>,
);
const trigger = screen.getByRole("button", { name: "작업" });
trigger.focus();
await user.keyboard("{ArrowDown}");
expect(screen.getByRole("menuitem", { name: "열기" })).toHaveFocus();
await user.keyboard("{ArrowDown}{Enter}");
expect(inspect).toHaveBeenCalledOnce();
await waitFor(() => expect(trigger).toHaveFocus());
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
});
it("supports manual tab activation with arrow-key roving focus", async () => {
const user = userEvent.setup();
render(
<Tabs
activation="manual"
label="계층"
tabs={[
{ id: "tokens", label: "토큰", panel: "토큰 내용" },
{ id: "patterns", label: "패턴", panel: "패턴 내용" },
]}
/>,
);
const tokenTab = screen.getByRole("tab", { name: "토큰" });
tokenTab.focus();
await user.keyboard("{ArrowRight}");
const patternTab = screen.getByRole("tab", { name: "패턴" });
expect(patternTab).toHaveFocus();
expect(patternTab).toHaveAttribute("aria-selected", "false");
await user.keyboard("{Enter}");
expect(patternTab).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("tabpanel", { name: "패턴" })).toHaveTextContent(
"패턴 내용",
);
});
it("uses a modal drawer and restores focus after Escape", async () => {
const user = userEvent.setup();
function Harness() {
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={() => setOpen(true)}> </Button>
<Drawer onClose={() => setOpen(false)} open={open} title="탐색">
<a href="/target"></a>
</Drawer>
</>
);
}
render(<Harness />);
const trigger = screen.getByRole("button", { name: "탐색 열기" });
await user.click(trigger);
expect(screen.getByRole("dialog", { name: "탐색" })).toHaveAttribute("open");
await user.keyboard("{Escape}");
await waitFor(() => expect(trigger).toHaveFocus());
});
it("bounds the toast queue and collapses duplicate IDs", async () => {
const user = userEvent.setup();
function Harness() {
const toast = useToast();
return (
<>
<Button
onClick={() =>
toast.push({ id: "same", title: "저장됨", durationMs: 60_000 })
}
>
</Button>
<Button
onClick={() => {
for (let index = 0; index < 5; index += 1) {
toast.push({
id: `toast-${index}`,
title: `알림 ${index}`,
durationMs: 60_000,
});
}
}}
>
</Button>
</>
);
}
render(
<ToastProvider limit={3}>
<Harness />
</ToastProvider>,
);
await user.click(screen.getByRole("button", { name: "중복" }));
await user.click(screen.getByRole("button", { name: "중복" }));
expect(screen.getByText("×2")).toBeVisible();
await user.click(screen.getByRole("button", { name: "다섯 개" }));
const region = screen.getByRole("region", { name: "알림" });
expect(within(region).getAllByRole("article")).toHaveLength(3);
expect(within(region).queryByText("알림 0")).not.toBeInTheDocument();
expect(within(region).getByText("알림 4")).toBeVisible();
});
it("pauses toast timeout while the user is interacting", () => {
vi.useFakeTimers();
function Harness() {
const toast = useToast();
return (
<Button
onClick={() =>
toast.push({ id: "timed", title: "시간 제한", durationMs: 1000 })
}
>
</Button>
);
}
render(
<ToastProvider>
<Harness />
</ToastProvider>,
);
fireEvent.click(screen.getByRole("button", { name: "표시" }));
const item = screen.getByRole("article");
fireEvent.mouseEnter(item);
act(() => vi.advanceTimersByTime(2000));
expect(item).toBeVisible();
fireEvent.mouseLeave(item);
act(() => vi.advanceTimersByTime(1000));
expect(screen.queryByText("시간 제한")).not.toBeInTheDocument();
vi.useRealTimers();
});
it("provides dismissible popover and data/navigation patterns", async () => {
const user = userEvent.setup();
const onPage = vi.fn();
render(
<>
<Popover triggerLabel="설명 열기">
<Button> </Button>
</Popover>
<DataTable
caption="결과"
columns={[
{ id: "name", header: "이름", cell: (row) => row.name },
]}
empty="비어 있음"
rowKey={(row) => row.id}
rows={[{ id: "one", name: "첫 항목" }]}
/>
<Pagination
label="페이지"
nextLabel="다음"
onChange={onPage}
page={1}
pageCount={2}
pageLabel={(page) => `${page}페이지`}
previousLabel="이전"
/>
</>,
);
const popoverTrigger = screen.getByRole("button", { name: "설명 열기" });
await user.click(popoverTrigger);
expect(screen.getByRole("dialog")).toHaveTextContent("내부 작업");
await user.keyboard("{Escape}");
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
expect(popoverTrigger).toHaveFocus();
expect(screen.getByRole("table", { name: "결과" })).toHaveTextContent(
"첫 항목",
);
await user.click(screen.getByRole("link", { name: "2페이지" }));
expect(onPage).toHaveBeenCalledWith(2);
});
});
+19
View File
@@ -0,0 +1,19 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { Button } from "../../src/presentation/components/ui/button.ts";
import { Card } from "../../src/presentation/components/ui/card.ts";
describe("design-token fixture", () => {
it("uses static semantic primitive classes", () => {
render(
<Card title="Design token fixture">
<Button>Token action</Button>
</Card>,
);
expect(screen.getByRole("article")).toHaveClass("ui-card");
expect(screen.getByRole("button")).toHaveClass("ui-button");
});
});
+280
View File
@@ -0,0 +1,280 @@
// @vitest-environment jsdom
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { createMemoryRouter, RouterProvider, useNavigate } from "react-router-dom";
import { z } from "zod";
import { describe, expect, it, vi } from "vitest";
import { createFailure } from "../../src/contracts/errors.ts";
import { Button } from "../../src/presentation/components/ui/button.ts";
import {
DirtyNavigationDialog,
ErrorSummary,
Form,
FormField,
useAppForm,
useDirtyNavigationGuard,
} from "../../src/presentation/forms/index.ts";
type Values = Readonly<Record<"name" | "note", string>>;
const schema = z
.object({
name: z.string().trim().min(2),
note: z.string().trim().default(""),
})
.strict();
const defaults: Values = { name: "", note: "" };
function FormHarness(props: Readonly<{
submit(command: Readonly<{ name: string; note?: string }>): Promise<
| Readonly<{ ok: true; value: string }>
| Readonly<{ ok: false; error: ReturnType<typeof createFailure> }>
>;
resetOnSuccess?: boolean;
}>) {
const form = useAppForm({
schema,
defaultValues: defaults,
allowedServerFields: ["name", "note"],
mapToCommand(values) {
return {
name: values.name,
...(values.note ? { note: values.note } : {}),
};
},
submit: props.submit,
resetOnSuccess: props.resetOnSuccess,
});
return (
<Form pending={form.pending} onSubmit={(event) => void form.submitForm(event)}>
<ErrorSummary
fieldErrors={form.fieldErrors}
formErrors={form.formErrors}
fieldLabels={{ name: "Name", note: "Note" }}
fieldId={form.fieldId}
onFocusField={form.focusField}
/>
<FormField {...form.field("name")} label="Name" required />
<FormField {...form.field("note")} label="Note" />
<Button type="submit" disabled={form.pending}>
{form.pending ? "Pending" : "Submit"}
</Button>
<Button onClick={() => form.reset()} disabled={!form.dirty}>
Reset
</Button>
<Button onClick={() => form.settleApplied()}>Confirm applied</Button>
<Button onClick={() => form.settleNotApplied()}>
Confirm not applied
</Button>
<output data-testid="dirty">{String(form.dirty)}</output>
<output data-testid="result">{form.result}</output>
</Form>
);
}
describe("local form facade", () => {
it("focuses the first invalid field and performs no command", async () => {
const user = userEvent.setup();
const submit = vi.fn();
render(<FormHarness submit={submit} />);
await user.click(screen.getByRole("button", { name: "Submit" }));
expect(submit).not.toHaveBeenCalled();
expect(screen.getByRole("textbox", { name: /Name/ })).toHaveFocus();
expect(screen.getByRole("alert")).toHaveTextContent("Name");
});
it("submits transformed data once and clears dirty state after success", async () => {
const user = userEvent.setup();
let finish: ((value: { ok: true; value: string }) => void) | undefined;
const submit = vi.fn(
() =>
new Promise<{ ok: true; value: string }>((resolve) => {
finish = resolve;
}),
);
render(<FormHarness submit={submit} />);
await user.type(screen.getByRole("textbox", { name: /Name/ }), " Ready ");
await user.type(screen.getByRole("textbox", { name: "Note" }), " Safe ");
await user.dblClick(screen.getByRole("button", { name: "Submit" }));
await waitFor(() => expect(submit).toHaveBeenCalledOnce());
expect(submit).toHaveBeenCalledWith({ name: "Ready", note: "Safe" });
expect(screen.getByRole("button", { name: "Pending" })).toBeDisabled();
finish?.({ ok: true, value: "saved" });
await waitFor(() => expect(screen.getByTestId("dirty")).toHaveTextContent("false"));
expect(screen.getByTestId("result")).toHaveTextContent("success");
});
it("maps only approved 422 fields and never renders backend copy", async () => {
const user = userEvent.setup();
const failure = createFailure(
"VALIDATION_REJECTED",
"CREATE_ENTITY",
0,
{
validationIssues: [
{ path: "name", code: "REQUIRED" },
{ path: "serverOnly", code: "raw-secret-message" },
],
},
);
render(
<FormHarness submit={async () => ({ ok: false, error: failure })} />,
);
await user.type(screen.getByRole("textbox", { name: /Name/ }), "Valid");
await user.click(screen.getByRole("button", { name: "Submit" }));
expect(await screen.findByRole("alert")).toHaveTextContent("필수 입력값입니다.");
expect(screen.getByRole("alert")).toHaveTextContent(
"서버가 확인하지 못한 입력 항목",
);
expect(document.body).not.toHaveTextContent("raw-secret-message");
});
it("keeps conflict input out of URL and storage", async () => {
const user = userEvent.setup();
localStorage.clear();
window.history.replaceState({}, "", "/form-test");
render(
<FormHarness
submit={async () => ({
ok: false,
error: createFailure("CONFLICT", "CREATE_ENTITY", 0),
})}
/>,
);
const secretLike = "token-like-do-not-copy";
await user.type(screen.getByRole("textbox", { name: /Name/ }), secretLike);
await user.click(screen.getByRole("button", { name: "Submit" }));
expect(await screen.findByText(/다른 변경과 충돌했습니다/)).toBeVisible();
expect(screen.getByRole("textbox", { name: /Name/ })).toHaveValue(secretLike);
expect(window.location.href).not.toContain(secretLike);
expect(JSON.stringify(localStorage)).not.toContain(secretLike);
});
it("blocks a second submit while the prior effect remains unknown", async () => {
const user = userEvent.setup();
const submit = vi.fn(async () => ({
ok: false as const,
error: createFailure("SERVER_FAILURE", "CREATE_ENTITY", 0, {
effect: "MAYBE_APPLIED",
}),
}));
render(<FormHarness submit={submit} resetOnSuccess={false} />);
const name = screen.getByRole("textbox", { name: /Name/ });
await user.type(name, "Alpha");
await user.click(screen.getByRole("button", { name: "Submit" }));
expect(await screen.findByTestId("result")).toHaveTextContent(
"effect-unknown",
);
await user.clear(name);
await user.type(name, "Beta");
await user.click(screen.getByRole("button", { name: "Submit" }));
expect(submit).toHaveBeenCalledOnce();
expect(screen.getByTestId("result")).toHaveTextContent("effect-unknown");
});
it("settles the submitted unknown snapshot without accepting later edits", async () => {
const user = userEvent.setup();
const submit = vi.fn(async () => ({
ok: false as const,
error: createFailure("SERVER_FAILURE", "CREATE_ENTITY", 0, {
effect: "MAYBE_APPLIED",
}),
}));
render(<FormHarness submit={submit} resetOnSuccess={false} />);
const name = screen.getByRole("textbox", { name: /Name/ });
await user.type(name, "Alpha");
await user.click(screen.getByRole("button", { name: "Submit" }));
await screen.findByText("effect-unknown");
await user.clear(name);
await user.type(name, "Beta");
await user.click(screen.getByRole("button", { name: "Confirm applied" }));
expect(name).toHaveValue("Beta");
expect(screen.getByTestId("result")).toHaveTextContent("success");
expect(screen.getByTestId("dirty")).toHaveTextContent("true");
await user.clear(name);
await user.type(name, "Alpha");
expect(screen.getByTestId("dirty")).toHaveTextContent("false");
});
it("releases an unknown submission only after explicit not-applied settlement", async () => {
const user = userEvent.setup();
const submit = vi
.fn()
.mockResolvedValueOnce({
ok: false as const,
error: createFailure("SERVER_FAILURE", "CREATE_ENTITY", 0, {
effect: "MAYBE_APPLIED",
}),
})
.mockResolvedValueOnce({ ok: true as const, value: "saved" });
render(<FormHarness submit={submit} resetOnSuccess={false} />);
await user.type(screen.getByRole("textbox", { name: /Name/ }), "Alpha");
await user.click(screen.getByRole("button", { name: "Submit" }));
await screen.findByText("effect-unknown");
await user.click(
screen.getByRole("button", { name: "Confirm not applied" }),
);
expect(screen.getByTestId("result")).toHaveTextContent("idle");
await user.click(screen.getByRole("button", { name: "Submit" }));
expect(submit).toHaveBeenCalledTimes(2);
});
});
describe("dirty navigation guard", () => {
it("blocks navigation, restores focus on stay and proceeds explicitly", async () => {
const user = userEvent.setup();
function GuardedPage() {
const navigate = useNavigate();
const [dirty, setDirty] = useState(false);
const guard = useDirtyNavigationGuard(dirty);
return (
<>
<label htmlFor="guard-field">Guard field</label>
<input
id="guard-field"
onChange={() => setDirty(true)}
/>
<Button onClick={() => navigate("/target")}>Leave</Button>
<DirtyNavigationDialog guard={guard} />
</>
);
}
const router = createMemoryRouter(
[
{ path: "/", element: <GuardedPage /> },
{ path: "/target", element: <h1>Target</h1> },
],
{ initialEntries: ["/"] },
);
render(<RouterProvider router={router} />);
await user.type(screen.getByRole("textbox", { name: "Guard field" }), "x");
const leave = screen.getByRole("button", { name: "Leave" });
await user.click(leave);
expect(
screen.getByRole("dialog", { name: "저장하지 않은 변경이 있습니다." }),
).toHaveAttribute("open");
await user.click(screen.getByRole("button", { name: "계속 작성" }));
await waitFor(() => expect(leave).toHaveFocus());
await user.click(leave);
await user.click(screen.getByRole("button", { name: "변경 버리고 이동" }));
expect(await screen.findByRole("heading", { name: "Target" })).toBeVisible();
});
});
+120
View File
@@ -0,0 +1,120 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { describe, expect, it } from "vitest";
import {
Button,
Drawer,
Pagination,
Tabs,
} from "../../src/presentation/design-system/index.ts";
import {
LocaleProvider,
useLocale,
type SupportedLocale,
} from "../../src/presentation/i18n/index.ts";
function LocaleHarness() {
const { direction, locale, message, setLocale } = useLocale();
const [drawerOpen, setDrawerOpen] = useState(false);
return (
<>
<label htmlFor="test-locale">{message("shell.locale")}</label>
<select
id="test-locale"
onChange={(event) =>
setLocale(event.currentTarget.value as SupportedLocale)
}
value={locale}
>
<option value="ko-KR"></option>
<option value="en-US">English</option>
<option value="ar-EG">RTL</option>
</select>
<output>{`${locale}:${direction}`}</output>
<Button onClick={() => setDrawerOpen(true)}>
{message("shell.menu")}
</Button>
<Drawer
closeLabel={message("shell.closeMenu")}
onClose={() => setDrawerOpen(false)}
open={drawerOpen}
title={message("shell.menu")}
>
content
</Drawer>
</>
);
}
describe("locale provider runtime", () => {
it("switches copy and synchronizes the document language and direction", async () => {
const user = userEvent.setup();
render(
<LocaleProvider>
<LocaleHarness />
</LocaleProvider>,
);
expect(document.documentElement).toHaveAttribute("lang", "ko-KR");
await user.selectOptions(screen.getByLabelText("언어"), "en-US");
expect(screen.getByLabelText("Language")).toHaveValue("en-US");
expect(document.documentElement).toHaveAttribute("lang", "en-US");
expect(document.documentElement).toHaveAttribute("dir", "ltr");
await user.selectOptions(screen.getByLabelText("Language"), "ar-EG");
expect(document.documentElement).toHaveAttribute("lang", "ar-EG");
expect(document.documentElement).toHaveAttribute("dir", "rtl");
await user.click(screen.getByRole("button", { name: "Menu" }));
expect(screen.getByRole("dialog", { name: "Menu" })).toHaveAttribute(
"open",
);
});
it("reverses horizontal tab focus semantics in RTL", async () => {
const user = userEvent.setup();
render(
<LocaleProvider initialLocale="ar-EG">
<Tabs
activation="manual"
label="sections"
tabs={[
{ id: "one", label: "One", panel: "One panel" },
{ id: "two", label: "Two", panel: "Two panel" },
{ id: "three", label: "Three", panel: "Three panel" },
]}
/>
</LocaleProvider>,
);
const first = screen.getByRole("tab", { name: "One" });
first.focus();
await user.keyboard("{ArrowRight}");
expect(screen.getByRole("tab", { name: "Three" })).toHaveFocus();
await user.keyboard("{ArrowLeft}");
expect(first).toHaveFocus();
});
it("keeps pagination semantics while direction-aware icons remain decorative", () => {
render(
<LocaleProvider initialLocale="ar-EG">
<Pagination
label="pages"
nextLabel="Next page"
onChange={() => {}}
page={2}
pageCount={3}
pageLabel={(page) => `Page ${page}`}
previousLabel="Previous page"
/>
</LocaleProvider>,
);
expect(
screen.getByRole("button", { name: "Previous page" }),
).toBeEnabled();
expect(screen.getByRole("button", { name: "Next page" })).toBeEnabled();
expect(screen.queryByRole("img")).not.toBeInTheDocument();
});
});
@@ -0,0 +1,52 @@
// @vitest-environment jsdom
import { render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { PageHeader } from "../../src/presentation/components/page-header.tsx";
describe("page header focus ownership", () => {
it("takes over focus handed off by the route main region", async () => {
const { rerender } = render(
<>
<main id="main-content" tabIndex={-1} />
<PageHeader title="Loading" />
</>,
);
const main = screen.getByRole("main");
main.focus();
rerender(
<>
<main id="main-content" tabIndex={-1} />
<PageHeader title="Loaded" />
</>,
);
await waitFor(() =>
expect(
screen.getByRole("heading", { level: 1, name: "Loaded" }),
).toHaveFocus(),
);
});
it("does not steal focus from a user-controlled element", () => {
const { rerender } = render(
<>
<button type="button">Menu</button>
<PageHeader title="Loading" />
</>,
);
const menu = screen.getByRole("button", { name: "Menu" });
menu.focus();
rerender(
<>
<button type="button">Menu</button>
<PageHeader title="Loaded" />
</>,
);
expect(menu).toHaveFocus();
});
});
+92
View File
@@ -0,0 +1,92 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import {
CollectionPage,
DetailPage,
FormPage,
StandardPage,
StatusPage,
} from "../../src/presentation/templates/index.ts";
describe("page template slot contracts", () => {
it("renders StandardPage minimum and full landmarks with one h1", () => {
const { rerender } = render(
<StandardPage heading={{ title: "Minimum" }}>Content</StandardPage>,
);
expect(screen.getByRole("heading", { level: 1, name: "Minimum" })).toBeVisible();
rerender(
<StandardPage
heading={{ title: "Full", description: "Long heading contract" }}
breadcrumb={<a href="/">Home</a>}
status={<span>Ready</span>}
actions={[
{ kind: "button", label: "Action", onAction: () => {} },
]}
notices={<p>Notice</p>}
feedback={<p role="status">Refreshing</p>}
aside={<p>Aside</p>}
>
Content
</StandardPage>,
);
expect(screen.getAllByRole("heading", { level: 1 })).toHaveLength(1);
expect(screen.getByRole("navigation", { name: "현재 위치" })).toBeVisible();
expect(screen.getByRole("complementary", { name: "관련 정보" })).toBeVisible();
});
it("places collection, detail and form state in stable slots", () => {
const { rerender } = render(
<CollectionPage
heading={{ title: "Collection" }}
toolbar={<button type="button">Filter</button>}
resultCount="12 results"
pagination={<a href="?page=2">Next</a>}
>
Results
</CollectionPage>,
);
expect(screen.getByRole("region", { name: "검색과 필터" })).toBeVisible();
expect(screen.getByRole("navigation", { name: "페이지 탐색" })).toBeVisible();
rerender(
<DetailPage
heading={{ title: "Detail" }}
metadata={<dl><dt>ID</dt><dd>1</dd></dl>}
destructiveAction={<button type="button">Delete</button>}
>
Sections
</DetailPage>,
);
expect(screen.getByRole("region", { name: "요약 정보" })).toBeVisible();
expect(screen.getByRole("region", { name: "위험 작업" })).toBeVisible();
rerender(
<FormPage
heading={{ title: "Form" }}
errorSummary={<p role="alert">Invalid</p>}
fields={<input aria-label="Field" />}
formActions={<button type="button">Save</button>}
/>,
);
expect(screen.getByRole("alert")).toBeVisible();
expect(screen.getByRole("textbox", { name: "Field" })).toBeVisible();
});
it("renders safe status variants without raw failure values", () => {
render(
<StatusPage
variant="offline"
heading={{ title: "Offline", description: "Safe recovery copy" }}
primaryAction={{ kind: "button", label: "Retry", onAction: () => {} }}
supportReference="SAFE-123"
/>,
);
expect(screen.getByRole("heading", { level: 1, name: "Offline" })).toBeVisible();
expect(screen.getByText(/SAFE-123/)).toBeVisible();
expect(document.body).not.toHaveTextContent("stack");
});
});
@@ -0,0 +1,164 @@
// @vitest-environment jsdom
import { render, screen, waitFor, within } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { SERVER_STATE_PROFILES } from "../../src/contracts/server-state.ts";
import { COMPOSED_CONTRACT_CONTRIBUTIONS } from "../../src/features/installed-contract-contributions.ts";
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.ts";
import PlatformOverviewPage from "../../src/presentation/examples/platform-overview-page.tsx";
import { LocaleProvider } from "../../src/presentation/i18n/index.ts";
import { ApplicationProvider } from "../../src/presentation/providers/application-provider.tsx";
import { createTestApplication } from "../helpers/create-test-application.ts";
import { createRuntimeCapabilitiesStub } from "../helpers/runtime-capabilities-stub.ts";
/**
* The page must stay a projection of the installed registries. Every assertion
* below derives its expectation from the same registry the page reads, so a
* template that removes its sample feature still satisfies this suite.
*/
function renderPage() {
return render(
<ApplicationProvider application={createTestApplication()}>
<LocaleProvider>
<PlatformOverviewPage />
</LocaleProvider>
</ApplicationProvider>,
);
}
function tableByCaption(caption: string): HTMLElement {
return screen.getByRole("table", { name: caption });
}
/** A metric is a `dt`/`dd` pair, which carries no ARIA role to query by. */
function metricByLabel(scope: HTMLElement, label: string): HTMLElement {
const term = within(scope).getByText(label).closest(".platform-metric");
if (!(term instanceof HTMLElement)) {
throw new Error(`No metric is labelled ${label}`);
}
return term;
}
describe("platform overview page", () => {
it("renders one route row per installed route registry entry", () => {
renderPage();
const table = tableByCaption("설치된 라우트 목록");
const dataRows = within(table).getAllByRole("row").slice(1);
expect(dataRows).toHaveLength(Object.keys(ROUTE_REGISTRY).length);
for (const definition of Object.values(ROUTE_REGISTRY)) {
expect(
within(table).getByText(definition.routeId),
).toBeInTheDocument();
}
});
it("renders one operation row per installed HTTP contract", () => {
const operationIds = [
...COMPOSED_CONTRACT_CONTRIBUTIONS.httpByOperationId.keys(),
];
renderPage();
if (operationIds.length === 0) {
expect(
screen.getByRole("heading", {
name: "설치된 HTTP 오퍼레이션이 없습니다.",
}),
).toBeVisible();
return;
}
const table = tableByCaption("설치된 HTTP 오퍼레이션");
expect(within(table).getAllByRole("row").slice(1)).toHaveLength(
operationIds.length,
);
for (const operationId of operationIds) {
expect(within(table).getByText(operationId)).toBeInTheDocument();
}
});
it("renders every fixed server-state profile with its own budget", () => {
renderPage();
const table = tableByCaption("서버 상태 프로파일");
for (const profile of Object.values(SERVER_STATE_PROFILES)) {
expect(within(table).getByText(profile.profileId)).toBeInTheDocument();
}
expect(within(table).getAllByRole("row").slice(1)).toHaveLength(
Object.keys(SERVER_STATE_PROFILES).length,
);
});
it("labels a capability that was never selected as unselected", () => {
renderPage();
const region = screen.getByRole("region", { name: "선택적 런타임 능력" });
expect(within(region).getAllByText("미선택")).toHaveLength(4);
expect(within(region).queryByText("운영자가 비활성화함")).toBeNull();
});
it("separates an operator disable from a capability that was never selected", () => {
render(
<ApplicationProvider
application={createTestApplication({
runtimeCapabilities: createRuntimeCapabilitiesStub({
SERVICE_WORKER: { selected: 1, active: 0, override: "DISABLED" },
OFFLINE_COMMANDS: { selected: 1, active: 1 },
}),
})}
>
<LocaleProvider>
<PlatformOverviewPage />
</LocaleProvider>
</ApplicationProvider>,
);
const region = screen.getByRole("region", { name: "선택적 런타임 능력" });
expect(within(region).getByText("운영자가 비활성화함")).toBeVisible();
expect(within(region).getByText("활성 (1)")).toBeVisible();
expect(within(region).getAllByText("미선택")).toHaveLength(2);
});
it("counts installed contract packages separately from template fixtures", () => {
const fixtures = COMPOSED_CONTRACT_CONTRIBUTIONS.contributions.filter(
(contribution) => contribution.source.kind === "TEMPLATE_FIXTURE",
).length;
renderPage();
const summary = screen.getByRole("region", { name: "설치 요약" });
const packages = metricByLabel(summary, "외부 계약 패키지");
expect(
within(packages).getByText(
`${COMPOSED_CONTRACT_CONTRIBUTIONS.externalPackages.length}`,
),
).toBeVisible();
expect(
within(packages).getByText(`템플릿 픽스처 ${fixtures}`),
).toBeVisible();
expect(
within(metricByLabel(summary, "라우트")).getByText(
`${Object.keys(ROUTE_REGISTRY).length}`,
),
).toBeVisible();
});
it("renders the verified release identity once the runtime resolves it", async () => {
renderPage();
const region = screen.getByRole("region", { name: "릴리스 신원" });
await waitFor(() =>
expect(
within(metricByLabel(region, "빌드")).getByText("test-build"),
).toBeVisible(),
);
expect(
within(metricByLabel(region, "릴리스")).getByText("test-release"),
).toBeVisible();
});
});
+126
View File
@@ -0,0 +1,126 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import type { ReactNode } from "react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { createFailure } from "../../src/contracts/errors.ts";
import { deriveAsyncState } from "../../src/application/view-models/async-state.ts";
import { AsyncSurface } from "../../src/presentation/components/async-surface.tsx";
import { BootErrorShell } from "../../src/presentation/boundaries/boot-error-shell.tsx";
import { FeatureBoundary } from "../../src/presentation/boundaries/render-error-boundary.tsx";
function Defect(): ReactNode {
throw new Error("raw render stack");
}
describe("render recovery boundaries", () => {
it("catches programmer defects and emits best-effort safe telemetry", () => {
const onRenderFailure = vi.fn();
render(
<FeatureBoundary
routeId="APP_HOME"
buildId="build-a"
onRenderFailure={onRenderFailure}
>
<Defect />
</FeatureBoundary>,
);
expect(screen.getByRole("alert")).toHaveTextContent(
"화면을 표시하지 못했습니다.",
);
expect(onRenderFailure).toHaveBeenCalledWith({
routeId: "APP_HOME",
buildId: "build-a",
boundaryName: "feature",
});
});
it("keeps normalized operational failures in normal async state", () => {
const state = deriveAsyncState({
failure: createFailure("SERVER_FAILURE", "LIST", 0),
});
render(
<FeatureBoundary routeId="APP_HOME" buildId="build-a">
<AsyncSurface state={state} />
</FeatureBoundary>,
);
expect(screen.getByRole("alert")).toHaveTextContent(
"요청을 완료하지 못했습니다.",
);
expect(screen.getByRole("alert")).toHaveAttribute(
"data-message-key",
"error.server_failure",
);
});
it("renders a safe boot shell with no endpoint or stack", () => {
render(
<BootErrorShell
kind="BOOT_CONFIG_FAILURE"
code="CONFIG_SCHEMA_INVALID"
buildId="build-a"
configSchemaVersion="1"
supportReference="build-a:CONFIG_SCHEMA_INVALID"
/>,
);
const shell = screen.getByRole("alert");
expect(shell).toHaveTextContent("build-a:CONFIG_SCHEMA_INVALID");
expect(shell).not.toHaveTextContent("https://");
expect(shell).not.toHaveTextContent("stack");
});
it("allows a boundary reset action without reloading the page", async () => {
let shouldThrow = true;
function Recoverable() {
if (shouldThrow) throw new Error("defect");
return <p>recovered</p>;
}
const user = userEvent.setup();
const view = render(
<FeatureBoundary routeId="APP_HOME" buildId="build-a">
<Recoverable />
</FeatureBoundary>,
);
shouldThrow = false;
await user.click(screen.getByRole("button", { name: "다시 시도" }));
view.rerender(
<FeatureBoundary routeId="APP_HOME" buildId="build-a">
<Recoverable />
</FeatureBoundary>,
);
expect(screen.getByText("recovered")).toBeVisible();
});
it("resets a route failure when the registered location key changes", async () => {
let shouldThrow = true;
function RouteContent() {
if (shouldThrow) throw new Error("route defect");
return <p>next route</p>;
}
const view = render(
<FeatureBoundary
routeId="APP_HOME"
buildId="build-a"
resetKey="/first"
>
<RouteContent />
</FeatureBoundary>,
);
expect(screen.getByRole("alert")).toBeVisible();
shouldThrow = false;
view.rerender(
<FeatureBoundary
routeId="APP_HOME"
buildId="build-a"
resetKey="/second"
>
<RouteContent />
</FeatureBoundary>,
);
expect(await screen.findByText("next route")).toBeVisible();
});
});
+100
View File
@@ -0,0 +1,100 @@
// @vitest-environment jsdom
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { createAnonymousSessionAdapter } from "../../src/adapters/auth/external-session-adapter.ts";
import { ApplicationProvider } from "../../src/presentation/providers/application-provider.tsx";
import { AppRouter } from "../../src/presentation/routes/app-router.tsx";
import { createTestApplication } from "../helpers/create-test-application.ts";
function renderRouter() {
return render(
<ApplicationProvider
application={createTestApplication({
session: createAnonymousSessionAdapter(),
})}
>
<AppRouter />
</ApplicationProvider>,
);
}
describe("generic application router", () => {
it("reaches the platform overview from the home starter actions", async () => {
const user = userEvent.setup();
window.history.pushState({}, "", "/");
renderRouter();
// The starter action lives inside the lazily loaded home chunk, so this is
// the first wait in the file that has to outlast a chunk load rather than
// an already-mounted shell element.
await user.click(
await screen.findByRole(
"link",
{ name: "플랫폼 구성 보기" },
{ timeout: 5000 },
),
);
await waitFor(() =>
expect(
screen.getByRole("heading", { name: "플랫폼 구성", level: 1 }),
).toHaveFocus(),
);
});
it("renders the app shell and not-found route without a feature input", async () => {
window.history.pushState({}, "", "/missing");
renderRouter();
expect(
await screen.findByRole("heading", {
name: "페이지를 찾을 수 없습니다.",
}),
).toBeVisible();
expect(screen.getByRole("navigation", { name: "주요 탐색" })).toBeVisible();
expect(screen.getByRole("main")).toBeVisible();
});
it("navigates between registry-backed platform routes", async () => {
const user = userEvent.setup();
window.history.pushState({}, "", "/");
renderRouter();
await user.click(
await screen.findByRole("link", { name: "UI 구성요소" }),
);
expect(
await screen.findByRole("heading", { name: "UI 구성요소", level: 1 }),
).toBeVisible();
expect(window.location.pathname).toBe("/examples/ui");
expect(document.title).toBe("UI 구성요소 · Frontend Skeleton");
await waitFor(() =>
expect(
screen.getByRole("heading", { name: "UI 구성요소", level: 1 }),
).toHaveFocus(),
);
});
it("focuses the route heading again when the lazy chunk is already cached", async () => {
// §9.7. The first visit resolves the route module asynchronously, so the
// router lifecycle and the page header commit separately. A later visit
// renders the cached module in the same commit, which is the ordering that
// must still hand focus to the heading rather than leaving it on main.
const user = userEvent.setup();
window.history.pushState({}, "", "/");
renderRouter();
for (const label of ["UI 구성요소", "화면 상태", "UI 구성요소"]) {
await user.click(await screen.findByRole("link", { name: label }));
await waitFor(() =>
expect(
screen.getByRole("heading", { name: label, level: 1 }),
).toHaveFocus(),
);
}
});
});
@@ -0,0 +1,74 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { createRuntimeComposition } from "../../src/bootstrap/create-runtime-composition.ts";
import { RuntimeApplication } from "../../src/bootstrap/runtime-application.tsx";
const runtimeConfig = {
APP_ENV: "local",
API_BASE_URL: "http://localhost:8080",
REQUEST_TIMEOUT_MS: 10_000,
MAX_RETRY_ATTEMPTS: 2,
TELEMETRY_ENABLED: false,
AUTH_MODE: "demo",
CONFIG_SCHEMA_VERSION: "1",
API_CONTRACT_VERSION: "1",
RELEASE_MANIFEST_URL: "/release-manifest.json",
BUILD_ID: "local-build",
RELEASE_ID: "local-release",
};
const releaseManifest = {
schemaVersion: 1,
appVersion: "0.1.0",
buildId: "local-build",
commitSha: "local",
configSchemaVersion: "1",
apiContractVersion: "1",
assetManifestHash: "test-hash",
releaseId: "local-release",
builtAt: "2026-07-26T00:00:00.000Z",
routeChunks: {
"route-home": "assets/home.js",
"route-examples-ui": "assets/ui.js",
"route-examples-states": "assets/states.js",
"route-examples-auth": "assets/auth.js",
"route-not-found": "assets/not-found.js",
},
};
describe("production runtime application tree", () => {
it("connects validated config and release through composition and ApplicationProvider", async () => {
const fetcher = vi.fn(async (input) => {
const url =
typeof input === "string"
? input
: input instanceof URL
? input.href
: input.url;
return Response.json(
url.includes("release-manifest") ? releaseManifest : runtimeConfig,
);
});
const composition = await createRuntimeComposition({
fetcher,
host: {},
});
window.history.pushState({}, "", "/");
render(<RuntimeApplication composition={composition} />);
expect(
await screen.findByRole("heading", {
name: "Clean Architecture Frontend",
}),
).toBeVisible();
expect(
await screen.findByText("빌드 local-build · 릴리스 local-release"),
).toBeVisible();
expect(fetcher).toHaveBeenCalledTimes(2);
expect(composition).not.toHaveProperty("ports");
});
});
@@ -0,0 +1,80 @@
// @vitest-environment jsdom
import { act, render, waitFor } from "@testing-library/react";
import { QueryClient, useQueryClient } from "@tanstack/react-query";
import { describe, expect, it } from "vitest";
import { createServerStateScopeRuntime } from "../../src/adapters/query-cache/server-state-scope-runtime.ts";
import { createServerStateGenerationStore } from "../../src/bootstrap/server-state-generation-store.ts";
import { ServerStateGenerationProvider } from "../../src/presentation/adapters/query/server-state-generation-provider.tsx";
describe("server-state generation provider", () => {
it("remounts consumers with the QueryClient owned by the READY generation", async () => {
const store = createServerStateGenerationStore(() => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
return {
queryClient,
queryInvalidation: {
invalidate: async () => {},
beginMutation: () => ({ release: async () => {} }),
resetLocal: async () => {
await queryClient.cancelQueries();
queryClient.clear();
},
dispose() {},
},
crossContextStatus: () => "DEGRADED_LOCAL_ONLY" as const,
};
});
let sessionListener: () => void = () => {};
let token = 0;
const scope = createServerStateScopeRuntime({
session: {
subscribe(listener) {
sessionListener = listener;
return () => {};
},
},
queryInvalidation: { resetLocal: () => store.resetCurrent() },
activateNextGeneration: () => store.activateNext(),
tokenFactory: () =>
`scope-generation-provider-${String(token++).padStart(4, "0")}`,
});
const renderedClients: QueryClient[] = [];
const mutationIntentFactory = Object.freeze({
create() {
throw new Error("mutation intent is unused by this provider test");
},
});
function Probe() {
renderedClients.push(useQueryClient());
return <div>generation-content</div>;
}
render(
<ServerStateGenerationProvider
store={store}
scope={scope}
mutationIntentFactory={mutationIntentFactory}
transitionFallback={<div>scope-transition</div>}
>
<Probe />
</ServerStateGenerationProvider>,
);
const firstClient = renderedClients.at(-1);
if (!firstClient) throw new Error("expected initial QueryClient");
act(() => sessionListener());
await waitFor(() => expect(scope.getPhase()).toBe("READY"));
await waitFor(() =>
expect(renderedClients.at(-1)).toBe(store.getSnapshot().queryClient),
);
expect(renderedClients.at(-1)).not.toBe(firstClient);
scope.dispose();
store.dispose();
});
});
@@ -0,0 +1,83 @@
// @vitest-environment jsdom
import { act, render, screen, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { createServerStateScopeRuntime } from "../../src/adapters/query-cache/server-state-scope-runtime.ts";
import { ServerStateScopeProvider } from "../../src/presentation/adapters/query/server-state-scope-provider.tsx";
const activeRuntimes: Array<{ dispose(): void }> = [];
afterEach(() => {
for (const runtime of activeRuntimes.splice(0)) runtime.dispose();
});
function scopeFixture(resetLocal: () => Promise<void>) {
let sessionListener: () => void = () => {};
let token = 0;
const runtime = createServerStateScopeRuntime({
session: {
subscribe(listener) {
sessionListener = listener;
return () => {};
},
},
queryInvalidation: {
resetLocal,
},
tokenFactory: () => `scope-provider-token-${String(token++).padStart(4, "0")}`,
});
activeRuntimes.push(runtime);
return { runtime, triggerSessionChange: () => sessionListener() };
}
describe("server-state scope provider", () => {
it("removes previous-scope children synchronously while reset is pending", async () => {
let completeReset: () => void = () => {};
const reset = new Promise<void>((resolve) => {
completeReset = resolve;
});
const fixture = scopeFixture(async () => reset);
render(
<ServerStateScopeProvider
runtime={fixture.runtime}
transitionFallback={<div>scope-transition</div>}
>
<div>previous-account-secret</div>
</ServerStateScopeProvider>,
);
expect(screen.getByText("previous-account-secret")).toBeVisible();
act(() => fixture.triggerSessionChange());
expect(screen.queryByText("previous-account-secret")).toBeNull();
expect(screen.getByText("scope-transition")).toBeVisible();
completeReset();
await waitFor(() =>
expect(screen.getByText("previous-account-secret")).toBeVisible(),
);
});
it("never remounts previous-scope children after mandatory cleanup failure", async () => {
const fixture = scopeFixture(async () => {
throw new Error("reset failed");
});
render(
<ServerStateScopeProvider
runtime={fixture.runtime}
transitionFallback={<div>scope-transition</div>}
>
<div>previous-account-secret</div>
</ServerStateScopeProvider>,
);
act(() => fixture.triggerSessionChange());
await waitFor(() => expect(fixture.runtime.getPhase()).toBe("FAILED"));
expect(screen.queryByText("previous-account-secret")).toBeNull();
expect(screen.getByText("scope-transition")).toBeVisible();
});
});
@@ -0,0 +1,20 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
type StatusProps = Readonly<{
label: string;
tone: "neutral" | "positive";
}>;
function Status({ label, tone }: StatusProps) {
return <output data-tone={tone}>{label}</output>;
}
describe("TSX test tooling", () => {
it("parses, lints, type-checks, and renders TSX", () => {
render(<Status label="ready" tone="positive" />);
expect(screen.getByText("ready")).toHaveAttribute("data-tone", "positive");
});
});
+108
View File
@@ -0,0 +1,108 @@
// @vitest-environment jsdom
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { describe, expect, it, vi } from "vitest";
import { Alert } from "../../src/presentation/components/ui/alert.ts";
import { Badge } from "../../src/presentation/components/ui/badge.ts";
import { Button } from "../../src/presentation/components/ui/button.ts";
import { Card } from "../../src/presentation/components/ui/card.ts";
import { Dialog } from "../../src/presentation/components/ui/dialog.ts";
import { TextField } from "../../src/presentation/components/ui/text-field.ts";
describe("domain-neutral UI primitives", () => {
it("connects field help and validation errors to the input", () => {
render(
<TextField
label="이름"
description="표시할 이름입니다."
error="이름을 입력해 주세요."
required
/>,
);
const field = screen.getByRole("textbox", { name: "이름" });
expect(field).toBeRequired();
expect(field).toHaveAccessibleDescription(
"표시할 이름입니다. 이름을 입력해 주세요.",
);
expect(field).toHaveAttribute("aria-invalid", "true");
});
it("exposes semantic variants without changing native button behavior", async () => {
const user = userEvent.setup();
const onClick = vi.fn();
render(
<>
<Button variant="danger" onClick={onClick}>
</Button>
<Button disabled> </Button>
</>,
);
await user.click(screen.getByRole("button", { name: "제거" }));
expect(onClick).toHaveBeenCalledOnce();
expect(screen.getByRole("button", { name: "제거" })).toHaveClass(
"ui-button--danger",
);
expect(screen.getByRole("button", { name: "사용 불가" })).toBeDisabled();
});
it("labels cards, alerts, and badges with visible content", async () => {
const user = userEvent.setup();
const dismiss = vi.fn();
render(
<Card title="상태 카드" footer={<Badge variant="success"></Badge>}>
<Alert title="저장됨" variant="success" onDismiss={dismiss}>
.
</Alert>
</Card>,
);
expect(screen.getByRole("article", { name: "상태 카드" })).toBeVisible();
expect(screen.getByRole("status")).toHaveTextContent(
"저장됨안전하게 반영했습니다.",
);
expect(screen.getByText("준비됨")).toHaveClass("ui-badge--success");
await user.click(screen.getByRole("button", { name: "저장됨 알림 닫기" }));
expect(dismiss).toHaveBeenCalledOnce();
});
it("closes a modal and restores focus to its trigger", async () => {
const user = userEvent.setup();
function DialogHarness() {
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={() => setOpen(true)}> </Button>
<Dialog
open={open}
onClose={() => setOpen(false)}
title="연동 확인"
actions={<Button onClick={() => setOpen(false)}></Button>}
>
</Dialog>
</>
);
}
render(<DialogHarness />);
const trigger = screen.getByRole("button", { name: "모달 열기" });
await user.click(trigger);
expect(screen.getByRole("dialog", { name: "연동 확인" })).toHaveAttribute(
"open",
);
await user.click(screen.getByRole("button", { name: "확인" }));
await waitFor(() => expect(trigger).toHaveFocus());
expect(screen.getByRole("dialog", { hidden: true })).not.toHaveAttribute(
"open",
);
});
});
+58
View File
@@ -0,0 +1,58 @@
import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "../support/browser/strict-browser-test.ts";
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.ts";
for (const route of Object.values(ROUTE_REGISTRY).map((definition) => {
if (definition.path === "*") return "/not-found";
return definition.path.replace(":resourceId", "reference-1");
})) {
test(`@a11y ${route} has no critical or serious axe violations`, async ({
page,
}) => {
await page.goto(route);
await expect(page.getByRole("main")).toBeVisible();
const results = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
.analyze();
const blocking = results.violations.filter((violation) =>
["critical", "serious"].includes(violation.impact ?? ""),
);
expect(blocking).toEqual([]);
});
}
test("@a11y keyboard reaches the primary route action with visible focus", async ({
page,
}) => {
await page.goto("/");
const action = page.getByRole("link", { name: "플랫폼 구성 보기" });
await expect(action).toBeVisible();
await page.keyboard.press("Tab");
await expect(action).toBeFocused();
await expect(action).toHaveCSS("outline-style", "solid");
});
test("@a11y reduced-motion policy disables long animation", async ({ page }) => {
await page.emulateMedia({ reducedMotion: "reduce" });
await page.goto("/");
const duration = await page
.locator("body")
.evaluate((body) => getComputedStyle(body).animationDuration);
expect(["0s", "0.00001s", "1e-05s"]).toContain(duration);
});
test("@a11y opened design-system dialog has no blocking violations", async ({
page,
}) => {
await page.goto("/examples/ui");
await page.getByRole("button", { name: "모달 열기" }).click();
const results = await new AxeBuilder({ page })
.include(".ui-dialog")
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
.analyze();
expect(
results.violations.filter((violation) =>
["critical", "serious"].includes(violation.impact ?? ""),
),
).toEqual([]);
});
+44
View File
@@ -0,0 +1,44 @@
import { expect, test } from "../support/browser/strict-browser-test.ts";
test("boots the public app shell", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("heading", { level: 1 })).toHaveText(
"Clean Architecture Frontend",
);
await expect(page.getByRole("navigation", { name: "주요 탐색" })).toBeVisible();
await expect(page.getByRole("main")).toBeVisible();
});
test("navigates to a registry-backed example without a page reload", async ({
page,
}) => {
await page.goto("/");
await page
.getByRole("navigation", { name: "주요 탐색" })
.getByRole("link", { name: "화면 상태" })
.click();
await expect(page).toHaveURL(/\/examples\/states$/);
await expect(
page.getByRole("heading", { level: 1, name: "화면 상태" }),
).toBeFocused();
});
test("provides an escape-dismissible mobile navigation", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/");
const menu = page.getByRole("button", { name: "메뉴", exact: true });
await menu.click();
await expect(menu).toHaveAttribute("aria-expanded", "true");
await expect(page.getByRole("navigation", { name: "주요 탐색" })).toBeVisible();
await expect(page.locator(".ui-drawer")).toHaveJSProperty("open", true);
expect(
await page.locator(".ui-drawer").evaluate((drawer) => drawer.matches(":modal")),
).toBe(true);
await page.keyboard.press("Escape");
await expect(menu).toHaveAttribute("aria-expanded", "false");
await expect(page.getByRole("navigation", { name: "주요 탐색" })).toBeHidden();
await expect(menu).toBeFocused();
});
+23
View File
@@ -0,0 +1,23 @@
import { expect, test } from "../support/browser/strict-browser-test.ts";
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();
const navigation = page.getByRole("navigation", { name: "주요 탐색" });
await expect(navigation).toBeVisible();
// The drawer is a non-modal dialog, so page content stays in the
// accessibility tree while it is open. Scope to the navigation and match the
// whole name, or a route call to action on the page behind it also matches.
await navigation
.getByRole("link", { name: "UI 구성요소", exact: true })
.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);
});
@@ -0,0 +1,29 @@
import { expect, test } from "../support/browser/strict-browser-test.ts";
test("runs menu typeahead, tabs and duplicate toast interactions", async ({
page,
}) => {
await page.goto("/examples/ui");
const menuTrigger = page.getByRole("button", { name: "작업 메뉴" });
await menuTrigger.focus();
await page.keyboard.press("ArrowDown");
await page.keyboard.type("toast");
const toastItem = page.getByRole("menuitem", { name: "Toast 표시" });
await expect(toastItem).toBeFocused();
await page.keyboard.press("Enter");
await expect(page.getByText("예제가 저장되었습니다.")).toBeVisible();
await expect(menuTrigger).toBeFocused();
const tokenTab = page.getByRole("tab", { name: "토큰" });
await tokenTab.focus();
await page.keyboard.press("ArrowRight");
const primitiveTab = page.getByRole("tab", { name: "프리미티브" });
await expect(primitiveTab).toBeFocused();
await expect(primitiveTab).toHaveAttribute("aria-selected", "false");
await page.keyboard.press("Enter");
await expect(primitiveTab).toHaveAttribute("aria-selected", "true");
await expect(
page.getByRole("tabpanel", { name: "프리미티브" }),
).toContainText("native semantics");
});
+41
View File
@@ -0,0 +1,41 @@
import { expect, test } from "../support/browser/strict-browser-test.ts";
test("switches the shell locale and keeps pseudo-locale copy within compact layout", async ({
page,
}) => {
await page.setViewportSize({ width: 320, height: 720 });
await page.goto("/");
const locale = page.getByRole("combobox", { name: "언어" });
await locale.selectOption("en-US");
await expect(page.locator("html")).toHaveAttribute("lang", "en-US");
await expect(page.locator("html")).toHaveAttribute("dir", "ltr");
await expect(page.getByRole("button", { name: "Menu" })).toBeVisible();
await page.getByRole("combobox", { name: "Language" }).selectOption("en-XA");
await expect(page.locator("html")).toHaveAttribute("lang", "en-XA");
const dimensions = await page.evaluate(() => ({
clientWidth: document.documentElement.clientWidth,
scrollWidth: document.documentElement.scrollWidth,
}));
expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth);
await expect(page.getByRole("button", { name: /Ménú/ })).toBeVisible();
});
test("applies RTL to the shell and modal navigation without changing action semantics", async ({
page,
}) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/");
await page.getByRole("combobox", { name: "언어" }).selectOption("ar-EG");
await expect(page.locator("html")).toHaveAttribute("lang", "ar-EG");
await expect(page.locator("html")).toHaveAttribute("dir", "rtl");
const trigger = page.getByRole("button", { name: "Menu" });
await trigger.click();
await expect(page.getByRole("dialog", { name: "Menu" })).toBeVisible();
await expect(page.getByRole("navigation", { name: "Primary navigation" }))
.toHaveCount(1);
await page.keyboard.press("Escape");
await expect(trigger).toBeFocused();
});
+58
View File
@@ -0,0 +1,58 @@
import { expect, test } from "../support/browser/strict-browser-test.ts";
import { SERVER_STATE_PROFILES } from "../../src/contracts/server-state.ts";
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.ts";
/**
* The overview is a projection of the installed registries. These assertions
* read the same registries the shipped bundle was built from, so they keep
* meaning after a feature is added or removed.
*/
test("projects the installed route registry into the shipped bundle", async ({
page,
}) => {
await page.goto("/examples/platform");
await expect(
page.getByRole("heading", { name: "플랫폼 구성", level: 1 }),
).toBeVisible();
const table = page.getByRole("table", { name: "설치된 라우트 목록" });
await expect(table.locator("tbody tr")).toHaveCount(
Object.keys(ROUTE_REGISTRY).length,
);
for (const routeId of Object.keys(ROUTE_REGISTRY)) {
await expect(table.getByText(routeId, { exact: true })).toBeVisible();
}
});
test("shows every fixed server-state profile", async ({ page }) => {
await page.goto("/examples/platform");
const table = page.getByRole("table", { name: "서버 상태 프로파일" });
await expect(table.locator("tbody tr")).toHaveCount(
Object.keys(SERVER_STATE_PROFILES).length,
);
});
test("states the release contract identity verified at boot", async ({
page,
}) => {
await page.goto("/examples/platform");
const release = page.locator(
"section[aria-labelledby='platform-release-title']",
);
await expect(release.getByText(/^sha256:/)).toBeVisible();
});
test("reports an unselected capability without claiming it was disabled", async ({
page,
}) => {
await page.goto("/examples/platform");
const capabilities = page.locator(
"section[aria-labelledby='platform-capabilities-title']",
);
await expect(capabilities.getByText("미선택")).toHaveCount(4);
await expect(capabilities.getByText("운영자가 비활성화함")).toHaveCount(0);
});
+50
View File
@@ -0,0 +1,50 @@
import type { Page } from "@playwright/test";
import { expect, test } from "../support/browser/strict-browser-test.ts";
async function openReferenceForm(page: Page) {
await page.goto("/examples/reference-resources/new");
await page.getByRole("button", { name: "로그인 시작" }).click();
await expect(
page.getByRole("heading", { name: "Reference resource 만들기" }),
).toBeVisible();
}
test("validates a reference form and focuses the first invalid field", async ({
page,
}) => {
await openReferenceForm(page);
await page.getByRole("button", { name: "저장" }).click();
const firstField = page.getByRole("textbox", { name: /새 항목 이름/ });
await expect(firstField).toBeFocused();
await expect(firstField).toHaveAttribute("aria-invalid", "true");
await expect(page.getByRole("alert")).toContainText("입력 내용을 확인해 주세요.");
});
test("guards dirty cancellation and restores focus when writing continues", async ({
page,
}) => {
await openReferenceForm(page);
await page
.getByRole("textbox", { name: /새 항목 이름/ })
.fill("Unsaved reference");
const cancel = page.getByRole("button", { name: "취소" });
await cancel.click();
await expect(
page.getByRole("dialog", { name: "저장하지 않은 변경이 있습니다." }),
).toBeVisible();
await page.getByRole("button", { name: "계속 작성" }).click();
await expect(cancel).toBeFocused();
await expect(page).toHaveURL(/\/examples\/reference-resources\/new$/);
});
test("keeps the form template within a 320px viewport", async ({ page }) => {
await page.setViewportSize({ width: 320, height: 720 });
await openReferenceForm(page);
const viewport = await page.evaluate(() => ({
clientWidth: document.documentElement.clientWidth,
scrollWidth: document.documentElement.scrollWidth,
}));
expect(viewport.scrollWidth).toBeLessThanOrEqual(viewport.clientWidth);
});
+32
View File
@@ -0,0 +1,32 @@
import { expect, test } from "../support/browser/strict-browser-test.ts";
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.ts";
test("opens the protected integration route through the local demo seam", async ({
page,
}) => {
const protectedRoute = ROUTE_REGISTRY.REFERENCE_RESOURCE_LIST;
await page.route(
"http://localhost:8080/api/reference-resources?*",
(route) =>
route.fulfill({
json: [
{
id: "browser-reference",
name: "Browser reference",
createdAt: "2026-07-26T00:00:00.000Z",
},
],
}),
);
await page.goto(protectedRoute.path);
await expect(
page.getByRole("heading", { name: "세션이 필요합니다." }),
).toBeVisible();
await page.getByRole("button", { name: "로그인 시작" }).click();
await expect(
page.getByRole("heading", { name: protectedRoute.title }),
).toBeVisible();
await expect(page.getByText("인증됨")).toBeVisible();
});
+36
View File
@@ -0,0 +1,36 @@
import { expect, test } from "../support/browser/strict-browser-test.ts";
test("reflows the UI gallery at the 320px minimum without horizontal overflow", async ({
page,
}) => {
await page.setViewportSize({ width: 320, height: 720 });
await page.goto("/examples/ui");
await expect(
page.getByRole("heading", { level: 1, name: "UI 구성요소" }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "메뉴", exact: true }),
).toBeVisible();
const viewport = await page.evaluate(() => ({
clientWidth: document.documentElement.clientWidth,
scrollWidth: document.documentElement.scrollWidth,
}));
expect(viewport.scrollWidth).toBeLessThanOrEqual(viewport.clientWidth);
});
test("keeps desktop navigation and two-column examples at wide viewports", async ({
page,
}) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto("/examples/ui");
await expect(page.getByRole("navigation", { name: "주요 탐색" })).toBeVisible();
await expect(
page.getByRole("button", { name: "메뉴", exact: true }),
).toBeHidden();
await expect(page.locator(".component-grid--two").first()).toHaveCSS(
"grid-template-columns",
/.+px .+px/,
);
});
+38
View File
@@ -0,0 +1,38 @@
import AxeBuilder from "@axe-core/playwright";
import { expect, test } from "../support/browser/strict-browser-test.ts";
test("persists an explicit color scheme through the storage contract", async ({
page,
}) => {
await page.goto("/");
const selector = page.getByRole("combobox", { name: "색상 테마" });
await selector.selectOption("dark");
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
await expect(page.locator("html")).toHaveCSS("color-scheme", "dark");
const results = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
.analyze();
expect(
results.violations.filter((violation) =>
["critical", "serious"].includes(violation.impact ?? ""),
),
).toEqual([]);
await page.reload();
await expect(selector).toHaveValue("dark");
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
});
test("tracks operating-system changes while system preference is selected", async ({
page,
}) => {
await page.emulateMedia({ colorScheme: "dark" });
await page.goto("/");
const selector = page.getByRole("combobox", { name: "색상 테마" });
await selector.selectOption("system");
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
await page.emulateMedia({ colorScheme: "light" });
await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
});
+34
View File
@@ -0,0 +1,34 @@
import { expect, test } from "../support/browser/strict-browser-test.ts";
test("validates and reports the common text-field flow", async ({ page }) => {
await page.goto("/examples/ui");
await page.getByRole("button", { name: "입력 확인" }).click();
const field = page.getByRole("textbox", { name: "프로젝트 이름" });
await expect(field).toHaveAttribute("aria-invalid", "true");
await expect(field).toHaveAccessibleDescription(
/프로젝트 이름을 입력해 주세요/,
);
await field.fill("Starter");
await page.getByRole("button", { name: "입력 확인" }).click();
await expect(page.getByText("“Starter” 입력을 확인했습니다.")).toBeVisible();
});
test("traps modal interaction and restores focus to the trigger", async ({
page,
}) => {
await page.goto("/examples/ui");
const trigger = page.getByRole("button", { name: "모달 열기" });
await trigger.click();
const dialog = page.getByRole("dialog", { name: "연동 확인" });
await expect(dialog).toBeVisible();
await expect(
dialog.getByRole("button", { name: "연동 확인 닫기" }),
).toBeFocused();
await page.keyboard.press("Escape");
await expect(dialog).toBeHidden();
await expect(trigger).toBeFocused();
});
@@ -0,0 +1,186 @@
import { describe, expect, it, vi } from "vitest";
import {
buildRouteUrl,
parseRouteInput,
} from "../../../src/presentation/routes/route-codecs.ts";
import {
mapReferenceOperation,
toReferenceView,
} from "../../../src/features/reference-feature/contracts/reference-mapper.ts";
import {
validateReferencePayload,
validateReferenceRequest,
} from "../../../src/features/reference-feature/contracts/reference-schemas.ts";
import {
REFERENCE_FEATURE_CONTRACT,
REFERENCE_FEATURE_ID,
REFERENCE_RESOURCE_INVALIDATION_TOPIC,
} from "../../../src/features/reference-feature/contracts/reference-feature-contract.ts";
import type { ReferenceFeatureInput } from "../../../src/features/reference-feature/application/reference-feature-api.ts";
import { createTestApplication } from "../../helpers/create-test-application.ts";
import { createReferenceFeatureInstalledInput } from "../../../src/features/reference-feature/adapters/create-reference-feature-input.ts";
describe("reference feature boundary contracts", () => {
it("propagates uncertain command effect certainty into the application failure", async () => {
const execute = vi.fn(async () => ({
kind: "CONTRACT_VIOLATION" as const,
violation: {
kind: "SUCCESS_SCHEMA_INVALID" as const,
operation: "VALIDATION" as const,
},
effect: "MAYBE_APPLIED" as const,
}));
const installed = createReferenceFeatureInstalledInput({
contractOperations: { execute },
});
await expect(
installed.input.createResource({ name: "uncertain" }),
).resolves.toMatchObject({
ok: false,
error: {
effect: "MAYBE_APPLIED",
retryable: false,
action: "contact-support",
},
});
});
it("round-trips one canonical filter through the URL codec", () => {
const filters = {
tags: ["open", "new"],
cursor: "a/b",
limit: 5,
};
const url = buildRouteUrl("REFERENCE_RESOURCE_LIST", { search: filters });
expect(url).toBe(
"/examples/reference-resources?cursor=a%2Fb&limit=5&tags=open&tags=new",
);
const parsed = parseRouteInput(
"REFERENCE_RESOURCE_LIST",
{},
new URL(url, "https://app.test").searchParams,
);
expect(parsed).toMatchObject({
success: true,
data: { search: filters },
});
});
it("rejects unknown search and malformed DTO before mapping", () => {
expect(
parseRouteInput(
"REFERENCE_RESOURCE_LIST",
{},
new URLSearchParams("unknown=value"),
),
).toEqual({ success: false, code: "ROUTE_SEARCH_INVALID" });
expect(
validateReferencePayload("ReferenceResourceListPayload", [
{ id: "unsafe", name: 42 },
]),
).toMatchObject({ success: false });
expect(
mapReferenceOperation("LIST_REFERENCE_RESOURCES", [
{ id: "unsafe", name: 42 },
]),
).toMatchObject({ ok: false });
});
it("normalizes request input and maps only owned domain fields", () => {
expect(
validateReferenceRequest("CreateReferenceResourceCommand", {
name: " Example ",
}),
).toMatchObject({ success: true, data: { name: "Example" } });
const model = mapReferenceOperation("CREATE_REFERENCE_RESOURCE", {
id: "reference-1",
name: "Example",
createdAt: "2026-07-26T00:00:00.000Z",
});
if (!model.ok || !("id" in model.value)) {
throw new Error("expected one model");
}
expect(toReferenceView(model.value)).toEqual({
resourceId: "reference-1",
title: "Example",
createdAt: "2026-07-26T00:00:00.000Z",
});
});
it("owns route and operation contributions in one removable contract", () => {
expect(Object.keys(REFERENCE_FEATURE_CONTRACT.routes)).toEqual([
"REFERENCE_RESOURCE_LIST",
"REFERENCE_RESOURCE_DETAIL",
"REFERENCE_RESOURCE_FORM",
"REFERENCE_RESOURCE_STATUS",
]);
expect(Object.keys(REFERENCE_FEATURE_CONTRACT.apiOperations)).toEqual([
"LIST_REFERENCE_RESOURCES",
"CREATE_REFERENCE_RESOURCE",
"GET_REFERENCE_RESOURCE",
]);
expect(
REFERENCE_FEATURE_CONTRACT.apiOperations.GET_REFERENCE_RESOURCE,
).toMatchObject({
pathSchema: "ReferenceResourceParams",
pathParameterNames: ["resourceId"],
providerId: "PRIMARY_API",
authProfileId: "REFERENCE_EXTERNAL_BEARER",
csrfProfileId: "NO_CSRF_BEARER",
});
});
it("contributes an identity-based invalidation graph and an explicit wire version", () => {
expect(REFERENCE_FEATURE_CONTRACT.invalidation).toEqual({
topics: [REFERENCE_RESOURCE_INVALIDATION_TOPIC],
namespaces: [
{ namespaceId: "reference-resource", namespaceVersion: 1 },
],
edges: [
{
topicId: REFERENCE_RESOURCE_INVALIDATION_TOPIC,
namespace: {
namespaceId: "reference-resource",
namespaceVersion: 1,
},
},
],
});
expect(REFERENCE_FEATURE_CONTRACT.topicVersions).toEqual([
{
topicId: REFERENCE_RESOURCE_INVALIDATION_TOPIC,
topicVersion: 1,
},
]);
});
it("returns the installed input through the typed application registry", () => {
const featureInput = {
listResources: async () => ({ ok: true as const, value: [] }),
createResource: async ({ name }) => ({
ok: true as const,
value: {
resourceId: "created",
title: name,
createdAt: null,
},
}),
getResource: async (resourceId) => ({
ok: true as const,
value: {
resourceId,
title: "Reference",
createdAt: null,
},
}),
} satisfies ReferenceFeatureInput;
const application = createTestApplication({
featureInputs: { [REFERENCE_FEATURE_ID]: featureInput },
});
expect(application.features.has(REFERENCE_FEATURE_ID)).toBe(true);
expect(application.features.get(REFERENCE_FEATURE_ID)).toBe(featureInput);
});
});
@@ -0,0 +1,71 @@
import { describe, expect, it, vi } from "vitest";
import { createHttpClient } from "../../../src/adapters/http/client.ts";
import { createDemoSessionAdapter } from "../../../src/adapters/auth/external-session-adapter.ts";
import {
createReferenceHttpGateway,
} from "../../../src/features/reference-feature/adapters/reference-http-gateway.ts";
import { createReferenceFeatureInput } from "../../../src/features/reference-feature/application/reference-feature-api.ts";
import { REFERENCE_FEATURE_CONTRACT } from "../../../src/features/reference-feature/contracts/reference-feature-contract.ts";
import { mapReferenceOperation } from "../../../src/features/reference-feature/contracts/reference-mapper.ts";
import {
validateReferencePayload,
validateReferenceRequest,
} from "../../../src/features/reference-feature/contracts/reference-schemas.ts";
describe("reference feature diagnostics correlation", () => {
it("preserves route, operation and request correlation through the vertical path", async () => {
const record = vi.fn();
const operations =
REFERENCE_FEATURE_CONTRACT.apiOperations as Readonly<
Record<
string,
ReturnType<
NonNullable<Parameters<typeof createHttpClient>[0]["getOperation"]>
>
>
>;
const client = createHttpClient({
baseUrl: "https://api.test",
authSession: createDemoSessionAdapter("authenticated"),
fetcher: async () =>
Response.json({
success: true,
data: [{ id: "reference-1", name: "Reference" }],
meta: { requestId: "safe-request", traceId: "safe-trace" },
}),
getOperation(operationId) {
const operation = operations[operationId];
if (!operation) throw new Error("Unregistered reference operation");
return operation;
},
validatePayload: validateReferencePayload,
validateRequest: validateReferenceRequest,
mapPayload: mapReferenceOperation,
correlationIdFactory: () => "reference-correlation",
diagnostics: { record },
scheduler: {
setTimeout: () => 1,
clearTimeout: () => {},
},
});
const application = createReferenceFeatureInput(
createReferenceHttpGateway(client),
);
await expect(
application.listResources({ limit: 20 }),
).resolves.toMatchObject({ ok: true });
expect(record).toHaveBeenCalledOnce();
expect(record).toHaveBeenCalledWith({
level: "info",
eventId: "http.request.completed",
context: expect.objectContaining({
route_id: "REFERENCE_RESOURCE_LIST",
operation_id: "LIST_REFERENCE_RESOURCES",
correlation_id: "reference-correlation",
outcome: "success",
}),
});
});
});
@@ -0,0 +1,106 @@
import { describe, expect, it, vi } from "vitest";
import { createFailure } from "../../../src/contracts/errors.ts";
import {
createReferenceHttpGateway,
type RawReferenceHttpExecutor,
} from "../../../src/features/reference-feature/adapters/reference-http-gateway.ts";
import type { ReferenceResource } from "../../../src/features/reference-feature/domain/reference-resource.ts";
const resources = Object.freeze({
first: Object.freeze({
id: "reference-1",
displayName: "First",
createdAt: null,
}),
created: Object.freeze({
id: "reference-2",
displayName: "Created",
createdAt: "2026-07-26T00:00:00.000Z",
}),
}) satisfies Readonly<Record<string, ReferenceResource>>;
describe("reference HTTP operation gateway", () => {
it("builds the exact registered request for every gateway operation", async () => {
const execute = vi
.fn<RawReferenceHttpExecutor["execute"]>()
.mockResolvedValueOnce({ ok: true, value: [resources.first] })
.mockResolvedValueOnce({ ok: true, value: resources.created })
.mockResolvedValueOnce({ ok: true, value: resources.first });
const gateway = createReferenceHttpGateway({ execute });
const signal = new AbortController().signal;
await expect(
gateway.list({ cursor: "next", limit: 20, tags: ["active"] }, { signal }),
).resolves.toEqual({ ok: true, value: [resources.first] });
await expect(
gateway.create({ name: "Created", note: "safe note" }),
).resolves.toEqual({ ok: true, value: resources.created });
await expect(
gateway.get("reference-1", { signal }),
).resolves.toEqual({ ok: true, value: resources.first });
expect(execute.mock.calls).toEqual([
[
{
operationId: "LIST_REFERENCE_RESOURCES",
routeId: "REFERENCE_RESOURCE_LIST",
searchParams: {
cursor: "next",
limit: 20,
tags: ["active"],
},
signal,
},
],
[
{
operationId: "CREATE_REFERENCE_RESOURCE",
routeId: "REFERENCE_RESOURCE_LIST",
body: { name: "Created", note: "safe note" },
},
],
[
{
operationId: "GET_REFERENCE_RESOURCE",
routeId: "REFERENCE_RESOURCE_DETAIL",
pathParams: { resourceId: "reference-1" },
signal,
},
],
]);
});
it("preserves a validated raw failure without casting it into success", async () => {
const failure = createFailure(
"SCHEMA_MISMATCH",
"GET_REFERENCE_RESOURCE",
0,
);
const execute = vi
.fn<RawReferenceHttpExecutor["execute"]>()
.mockResolvedValue({ ok: false, error: failure });
const gateway = createReferenceHttpGateway({ execute });
await expect(gateway.get("invalid")).resolves.toEqual({
ok: false,
error: failure,
});
});
it("fails closed when a raw success does not match its operation result", async () => {
const execute = vi
.fn<RawReferenceHttpExecutor["execute"]>()
.mockResolvedValue({ ok: true, value: { id: "not-a-list" } });
const gateway = createReferenceHttpGateway({ execute });
await expect(gateway.list({ limit: 20 })).resolves.toMatchObject({
ok: false,
error: {
kind: "MAPPING_CONTRACT_VIOLATION",
code: "BOUND_RESULT_TYPE_MISMATCH",
operationId: "LIST_REFERENCE_RESOURCES",
},
});
});
});
@@ -0,0 +1,490 @@
// @vitest-environment jsdom
import {
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { createDemoSessionAdapter } from "../../../src/adapters/auth/external-session-adapter.ts";
import type { AuthSessionPort } from "../../../src/application/ports/auth-session-port.ts";
import type { MutationIntentFactory } from "../../../src/application/ports/mutation-intent-factory.ts";
import type {
ReferenceFeatureInput,
ReferenceResult,
} from "../../../src/features/reference-feature/application/reference-feature-api.ts";
import {
REFERENCE_FEATURE_ID,
REFERENCE_RESOURCE_INVALIDATION_TOPIC,
REFERENCE_RESOURCE_QUERY_NAMESPACE,
} from "../../../src/features/reference-feature/contracts/reference-feature-contract.ts";
import { INVALIDATION_REGISTRY } from "../../../src/features/installed-feature-contracts.ts";
import type { ReferenceResourceView } from "../../../src/features/reference-feature/contracts/reference-mapper.ts";
import { createFailure } from "../../../src/contracts/errors.ts";
import type { QueryInvalidationCoordinator } from "../../../src/contracts/query-invalidation.ts";
import { QueryInvalidationProvider } from "../../../src/presentation/adapters/query/query-invalidation-provider.tsx";
import { MutationIntentProvider } from "../../../src/presentation/adapters/query/mutation-intent-provider.tsx";
import { ServerStateScopeProvider } from "../../../src/presentation/adapters/query/server-state-scope-provider.tsx";
import { createServerStateScopeRuntime } from "../../../src/adapters/query-cache/server-state-scope-runtime.ts";
import { ApplicationProvider } from "../../../src/presentation/providers/application-provider.tsx";
import { AppRouter } from "../../../src/presentation/routes/app-router.tsx";
import { createTestApplication } from "../../helpers/create-test-application.ts";
function renderReference(
input: ReferenceFeatureInput,
url = "/examples/reference-resources?limit=5",
session: AuthSessionPort = createDemoSessionAdapter("authenticated"),
) {
window.history.pushState({}, "", url);
const client = new QueryClient({
defaultOptions: {
queries: { retry: false, gcTime: Infinity },
mutations: { retry: false },
},
});
const invalidation: QueryInvalidationCoordinator = Object.freeze({
async invalidate() {},
beginMutation() {
return Object.freeze({
async release() {},
});
},
async resetLocal() {},
dispose() {},
});
const serverStateScope = createServerStateScopeRuntime({
session,
queryInvalidation: invalidation,
});
let intentSequence = 0;
const mutationIntentFactory: MutationIntentFactory = Object.freeze({
create(input) {
intentSequence += 1;
return Object.freeze({
intentId: `reference-page-intent-${intentSequence}`,
operationId: input.operationId,
canonicalInputIdentity: input.canonicalInputIdentity,
...(input.requiresIdempotencyKey
? { idempotencyKey: `reference-page-key-${intentSequence}` }
: {}),
createdAtMonotonicMs: intentSequence,
});
},
});
return Object.assign(render(
<MutationIntentProvider factory={mutationIntentFactory}>
<QueryClientProvider client={client}>
<ServerStateScopeProvider runtime={serverStateScope}>
<QueryInvalidationProvider coordinator={invalidation}>
<ApplicationProvider
application={createTestApplication({
session,
featureInputs: { [REFERENCE_FEATURE_ID]: input },
})}
>
<AppRouter />
</ApplicationProvider>
</QueryInvalidationProvider>
</ServerStateScopeProvider>
</QueryClientProvider>
</MutationIntentProvider>,
), { client });
}
function inputWith(
overrides: Partial<ReferenceFeatureInput> = {},
): ReferenceFeatureInput {
return {
listResources: async () => ({ ok: true, value: [] }),
createResource: async ({ name }) => ({
ok: true,
value: {
resourceId: "created",
title: name,
createdAt: null,
},
}),
getResource: async (resourceId) => ({
ok: true,
value: {
resourceId,
title: "Detail",
createdAt: null,
},
}),
...overrides,
};
}
describe("reference feature page states", () => {
it("mounts list and detail keys under the installed governed namespace", async () => {
expect(REFERENCE_RESOURCE_QUERY_NAMESPACE).toEqual({
namespaceId: "reference-resource",
namespaceVersion: 1,
});
const installedEdge = INVALIDATION_REGISTRY.edges.find(
(edge) => edge.topicId === REFERENCE_RESOURCE_INVALIDATION_TOPIC,
);
expect(installedEdge?.namespace).toEqual(
REFERENCE_RESOURCE_QUERY_NAMESPACE,
);
const list = renderReference(inputWith());
await screen.findByRole("heading", { name: "표시할 항목이 없습니다." });
const listKey = list.client.getQueryCache().getAll()[0]?.queryKey;
expect(listKey?.slice(0, 4)).toEqual([
"query",
2,
REFERENCE_RESOURCE_QUERY_NAMESPACE.namespaceId,
REFERENCE_RESOURCE_QUERY_NAMESPACE.namespaceVersion,
]);
list.unmount();
const detail = renderReference(
inputWith(),
"/examples/reference-resources/reference-1",
);
await screen.findByText("Detail");
const detailKey = detail.client.getQueryCache().getAll()[0]?.queryKey;
expect(detailKey?.slice(0, 4)).toEqual([
"query",
2,
installedEdge?.namespace.namespaceId,
installedEdge?.namespace.namespaceVersion,
]);
detail.unmount();
});
it("renders loading, success and empty states through the installed route", async () => {
let resolveList:
| ((result: ReferenceResult<readonly ReferenceResourceView[]>) => void)
| undefined;
const pending = new Promise<
ReferenceResult<readonly ReferenceResourceView[]>
>((resolve) => {
resolveList = resolve;
});
const loaded = renderReference(
inputWith({ listResources: async () => pending }),
);
expect(await screen.findByLabelText("불러오는 중")).toBeVisible();
resolveList?.({
ok: true,
value: [
{
resourceId: "reference-1",
title: "Loaded",
createdAt: null,
},
],
});
expect(await screen.findByText("Loaded")).toBeVisible();
loaded.unmount();
renderReference(inputWith(), "/examples/reference-resources?limit=10");
expect(
await screen.findByRole("heading", {
name: "표시할 항목이 없습니다.",
}),
).toBeVisible();
});
it("rejects invalid URL input before the feature application input", async () => {
const listResources = vi.fn();
renderReference(
inputWith({ listResources }),
"/examples/reference-resources?limit=invalid",
);
expect(
await screen.findByRole("heading", {
name: "올바르지 않은 주소입니다.",
}),
).toBeVisible();
expect(listResources).not.toHaveBeenCalled();
});
it("renders backend forbidden even when the client access hint allowed entry", async () => {
const user = userEvent.setup();
renderReference(
inputWith({
listResources: async () => ({
ok: false,
error: createFailure(
"FORBIDDEN",
"LIST_REFERENCE_RESOURCES",
0,
),
}),
}),
);
expect(await screen.findByRole("alert")).toHaveTextContent(
"이 작업을 수행할 권한이 없습니다.",
);
await user.click(
screen.getByRole("button", { name: "안전한 화면으로 이동" }),
);
expect(
await screen.findByRole("heading", {
level: 1,
name: "Clean Architecture Frontend",
}),
).toBeVisible();
});
it("starts sign-in with the current route for a backend auth failure", async () => {
const user = userEvent.setup();
const demoSession = createDemoSessionAdapter("authenticated");
const beginSignIn = vi.fn(async () => {});
const session = { ...demoSession, beginSignIn };
renderReference(
inputWith({
listResources: async () => ({
ok: false,
error: createFailure(
"AUTH_REQUIRED",
"LIST_REFERENCE_RESOURCES",
0,
),
}),
}),
"/examples/reference-resources?limit=5",
session,
);
await user.click(await screen.findByRole("button", { name: "로그인" }));
expect(beginSignIn).toHaveBeenCalledWith(
"/examples/reference-resources?limit=5",
);
});
it("falls back to a public route when starting sign-in fails", async () => {
const user = userEvent.setup();
const demoSession = createDemoSessionAdapter("authenticated");
const beginSignIn = vi.fn(async () => {
throw new Error("identity provider unavailable");
});
renderReference(
inputWith({
listResources: async () => ({
ok: false,
error: createFailure(
"AUTH_REQUIRED",
"LIST_REFERENCE_RESOURCES",
0,
),
}),
}),
"/examples/reference-resources?limit=5",
{ ...demoSession, beginSignIn },
);
await user.click(await screen.findByRole("button", { name: "로그인" }));
expect(
await screen.findByRole("heading", {
level: 1,
name: "Clean Architecture Frontend",
}),
).toBeVisible();
});
it("deduplicates create, preserves input and surfaces a conflict", async () => {
const user = userEvent.setup();
let finish:
| ((result: ReferenceResult<ReferenceResourceView>) => void)
| undefined;
const createResource = vi.fn(
() =>
new Promise<ReferenceResult<ReferenceResourceView>>((resolve) => {
finish = resolve;
}),
);
renderReference(
inputWith({ createResource }),
"/examples/reference-resources/new",
);
await screen.findByRole("heading", {
name: "Reference resource 만들기",
});
await user.type(
screen.getByRole("textbox", { name: /새 항목 이름/ }),
"Conflicting",
);
await user.type(screen.getByLabelText("설명"), "Keep this input");
const submit = screen.getByRole("button", { name: "저장" });
await user.dblClick(submit);
await waitFor(() => expect(createResource).toHaveBeenCalledOnce());
expect(screen.getByRole("button", { name: "저장 중…" })).toBeDisabled();
finish?.({
ok: false,
error: createFailure(
"CONFLICT",
"CREATE_REFERENCE_RESOURCE",
0,
{ effect: "NOT_APPLIED" },
),
});
expect(await screen.findByText(/다른 변경과 충돌했습니다/)).toBeVisible();
expect(
screen.getByRole("textbox", { name: /새 항목 이름/ }),
).toHaveValue("Conflicting");
expect(screen.getByLabelText("설명")).toHaveValue("Keep this input");
});
it("blocks resubmit and exposes only reconciliation for an unknown create effect", async () => {
const user = userEvent.setup();
const createResource = vi.fn(async () => ({
ok: false as const,
error: createFailure(
"SERVER_FAILURE",
"CREATE_REFERENCE_RESOURCE",
0,
{ effect: "MAYBE_APPLIED" },
),
}));
renderReference(
inputWith({ createResource }),
"/examples/reference-resources/new",
);
await user.type(
await screen.findByRole("textbox", { name: /새 항목 이름/ }),
"Unknown result",
);
await user.click(screen.getByRole("button", { name: "저장" }));
expect(
await screen.findByText("변경 결과를 확인할 수 없습니다."),
).toBeVisible();
expect(screen.getByRole("button", { name: "저장" })).toBeDisabled();
expect(
screen.queryByText("저장하지 못했습니다. 잠시 후 다시 시도해 주세요."),
).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "저장" }));
expect(createResource).toHaveBeenCalledOnce();
expect(
screen.getByRole("textbox", { name: /새 항목 이름/ }),
).toHaveValue("Unknown result");
await user.click(
screen.getByRole("button", { name: "변경되지 않음으로 확인" }),
);
await waitFor(() =>
expect(screen.getByRole("button", { name: "저장" })).toBeEnabled(),
);
expect(createResource).toHaveBeenCalledOnce();
expect(
screen.getByRole("textbox", { name: /새 항목 이름/ }),
).toHaveValue("Unknown result");
});
it("settles the form after confirming an unknown create was applied", async () => {
const user = userEvent.setup();
const createResource = vi.fn(async () => ({
ok: false as const,
error: createFailure(
"SERVER_FAILURE",
"CREATE_REFERENCE_RESOURCE",
0,
{ effect: "MAYBE_APPLIED" },
),
}));
renderReference(
inputWith({ createResource }),
"/examples/reference-resources/new",
);
const name = await screen.findByRole("textbox", {
name: /새 항목 이름/,
});
await user.type(name, "Already created");
await user.click(screen.getByRole("button", { name: "저장" }));
await user.click(
await screen.findByRole("button", { name: "변경됨으로 확인" }),
);
expect(await screen.findByRole("status")).toHaveTextContent("저장했습니다.");
expect(name).toHaveValue("");
await user.click(screen.getByRole("button", { name: "저장" }));
expect(createResource).toHaveBeenCalledOnce();
});
it("treats an applied-confirmed failure as a settled create", async () => {
const user = userEvent.setup();
const createResource = vi.fn(async () => ({
ok: false as const,
error: createFailure(
"SERVER_FAILURE",
"CREATE_REFERENCE_RESOURCE",
0,
{ effect: "APPLIED_CONFIRMED" },
),
}));
renderReference(
inputWith({ createResource }),
"/examples/reference-resources/new",
);
const name = await screen.findByRole("textbox", {
name: /새 항목 이름/,
});
await user.type(name, "Committed despite response");
await user.click(screen.getByRole("button", { name: "저장" }));
expect(await screen.findByRole("status")).toHaveTextContent("저장했습니다.");
expect(name).toHaveValue("");
expect(
screen.queryByText("저장하지 못했습니다. 잠시 후 다시 시도해 주세요."),
).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "저장" }));
expect(createResource).toHaveBeenCalledOnce();
});
it("keeps stale data visible during refresh failure and recovers on retry", async () => {
const user = userEvent.setup();
const listResources = vi
.fn<ReferenceFeatureInput["listResources"]>()
.mockResolvedValueOnce({
ok: true,
value: [
{
resourceId: "existing",
title: "Existing",
createdAt: null,
},
],
})
.mockResolvedValueOnce({
ok: false,
error: createFailure(
"SERVER_FAILURE",
"LIST_REFERENCE_RESOURCES",
0,
),
})
.mockResolvedValue({
ok: true,
value: [
{
resourceId: "recovered",
title: "Recovered",
createdAt: null,
},
],
});
renderReference(inputWith({ listResources }));
await screen.findByText("Existing");
await user.click(screen.getByRole("button", { name: "새로고침" }));
expect(
await screen.findByText("기존 정보를 표시하고 있습니다."),
).toBeVisible();
expect(screen.getByText("Existing")).toBeVisible();
await user.click(screen.getByRole("button", { name: "다시 시도" }));
expect(await screen.findByText("Recovered")).toBeVisible();
await waitFor(() =>
expect(screen.queryByText("stale-degraded")).not.toBeInTheDocument(),
);
expect(listResources).toHaveBeenCalledTimes(3);
});
});
@@ -0,0 +1,121 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {
afterAll,
afterEach,
beforeAll,
describe,
expect,
it,
vi,
} from "vitest";
import { createRuntimeComposition } from "../../../src/bootstrap/create-runtime-composition.ts";
import { RuntimeApplication } from "../../../src/bootstrap/runtime-application.tsx";
import { createBootstrapHandlers } from "../../mocks/handlers/bootstrap.ts";
import { createReferenceScenarioHandlers } from "../../mocks/handlers/reference-resources.ts";
import { createStrictMockServer } from "../../mocks/server.ts";
const runtimeConfig = {
APP_ENV: "local",
API_BASE_URL: "https://api.test",
REQUEST_TIMEOUT_MS: 10_000,
MAX_RETRY_ATTEMPTS: 0,
TELEMETRY_ENABLED: false,
AUTH_MODE: "demo",
CONFIG_SCHEMA_VERSION: "1",
API_CONTRACT_VERSION: "1",
RELEASE_MANIFEST_URL: "/release-manifest.json",
BUILD_ID: "local-build",
RELEASE_ID: "local-release",
};
const releaseManifest = {
schemaVersion: 1,
appVersion: "0.1.0",
buildId: "local-build",
commitSha: "local",
configSchemaVersion: "1",
apiContractVersion: "1",
assetManifestHash: "test-hash",
releaseId: "local-release",
builtAt: "2026-07-26T00:00:00.000Z",
routeChunks: {
"route-home": "assets/home.js",
"route-examples-ui": "assets/ui.js",
"route-examples-states": "assets/states.js",
"route-examples-auth": "assets/auth.js",
"route-reference-resources": "assets/reference.js",
"route-reference-resource-detail": "assets/reference-detail.js",
"route-reference-resource-form": "assets/reference-form.js",
"route-reference-resource-status": "assets/reference-status.js",
"route-not-found": "assets/not-found.js",
},
};
const listRequests = vi.fn();
const createRequests = vi.fn();
const resources = [{ id: "reference-1", name: "Existing" }];
const mockApi = createStrictMockServer(
...createBootstrapHandlers(runtimeConfig, releaseManifest),
...createReferenceScenarioHandlers({
resources,
onList: listRequests,
onCreate: createRequests,
}),
);
beforeAll(mockApi.listen);
afterEach(() => {
mockApi.reset();
listRequests.mockClear();
createRequests.mockClear();
resources.splice(1);
});
afterAll(mockApi.close);
const absoluteFetch: typeof fetch = (input, init) => {
if (input instanceof Request) return fetch(input, init);
const url = new URL(
input instanceof URL ? input.href : input,
"http://app.test",
);
return fetch(url, init);
};
describe("reference feature production vertical path", () => {
it("traverses bootstrap, router, application, HTTP schema/mapper and query cache", async () => {
const user = userEvent.setup();
const composition = await createRuntimeComposition({
fetcher: absoluteFetch,
host: {},
});
window.history.pushState(
{},
"",
"/examples/reference-resources?tags=open&tags=new&limit=5",
);
render(<RuntimeApplication composition={composition} />);
await user.click(
await screen.findByRole("button", { name: "로그인 시작" }),
);
expect(await screen.findByText("Existing")).toBeVisible();
expect(listRequests).toHaveBeenCalledWith(
"?limit=5&tags=open&tags=new",
);
await user.click(screen.getByRole("button", { name: "새 항목 만들기" }));
await user.type(
await screen.findByRole("textbox", { name: /새 항목 이름/ }),
" Created ",
);
await user.click(screen.getByRole("button", { name: "저장" }));
expect(await screen.findByText("저장했습니다.")).toBeVisible();
await user.click(screen.getByRole("button", { name: "목록으로 돌아가기" }));
expect(await screen.findByText("Created")).toBeVisible();
expect(createRequests).toHaveBeenCalledWith({ name: "Created" });
expect(listRequests.mock.calls.length).toBeGreaterThanOrEqual(2);
});
});
@@ -0,0 +1,195 @@
import { describe, expect, it, vi } from "vitest";
import { createRuntimeAdapters } from "../../../src/bootstrap/runtime-adapters.ts";
import { createRuntimeIdentityRegistry } from "../../../src/contracts/query-keys.ts";
import { bindQuery } from "../../../src/contracts/server-state.ts";
import type { CacheScopeSnapshot } from "../../../src/contracts/server-state-scope.ts";
import {
REFERENCE_FEATURE_ID,
REFERENCE_RESOURCE_INVALIDATION_TOPIC,
} from "../../../src/features/reference-feature/contracts/reference-feature-contract.ts";
type Runtime = Parameters<typeof createRuntimeAdapters>[0]["runtime"];
type Release = Parameters<typeof createRuntimeAdapters>[0]["release"];
const runtime: Runtime = {
config: {
APP_ENV: "local",
API_BASE_URL: "http://localhost:8080",
TELEMETRY_ENABLED: false,
AUTH_MODE: "demo",
REQUEST_TIMEOUT_MS: 4321,
MAX_RETRY_ATTEMPTS: 0,
RELEASE_MANIFEST_URL: "/release-manifest.json",
CONFIG_SCHEMA_VERSION: "2.0",
CAPABILITY_OVERRIDES: {
REALTIME: "DEFAULT",
WEB_WORKER: "DEFAULT",
SERVICE_WORKER: "DEFAULT",
OFFLINE_COMMANDS: "DEFAULT",
},
},
configSchema: "V2",
build: {
buildId: "build-a",
commitSha: "abc123",
routerBasePath: "/",
runtimeConfigUrl: "/config.json",
},
validationDurationMs: 0,
};
const release: Release = {
schemaVersion: 2,
appVersion: "0.1.0",
buildId: "build-a",
commitSha: "abc123",
configSchemaVersion: "2.0",
contractSet: {
setAlgorithm: "CA_CONTRACT_SET_V1",
setDigest: `sha256:${"0".repeat(64)}`,
packages: [],
},
assetManifestHash: "hash-a",
releaseId: "release-a",
builtAt: "2026-07-25T00:00:00Z",
routeChunks: { "route-home": "assets/home.js" },
};
function referenceBoundQueryKey() {
const scope: CacheScopeSnapshot = {
generation: 1,
fingerprint: "runtime-scope-fingerprint-0001",
identities: createRuntimeIdentityRegistry({
tokenFactory: () => "runtime-identity-token-0001",
}),
signal: new AbortController().signal,
isCurrent: () => true,
};
return bindQuery(
{
definitionId: "reference-resource-runtime-test-v1",
definitionVersion: 1,
owner: REFERENCE_FEATURE_ID,
namespace: "reference-resource",
namespaceVersion: 1,
operationId: "GET_REFERENCE_RESOURCE",
profileId: "DETAIL_STANDARD",
measureResult: () => ({ itemCount: 1, estimatedBytes: 8 }),
execute: async () => ({ ok: true as const, value: "reference-1" }),
},
"reference-1",
scope,
).queryKey;
}
describe("reference feature runtime composition", () => {
it("invalidates a real bound query through the installed production graph", async () => {
const adapters = await createRuntimeAdapters({ runtime, release, host: {} });
const queryKey = referenceBoundQueryKey();
adapters.infrastructure.queryClient.setQueryData(queryKey, {
resourceId: "reference-1",
});
await adapters.infrastructure.queryInvalidation.invalidate([
REFERENCE_RESOURCE_INVALIDATION_TOPIC,
]);
expect(
adapters.infrastructure.queryClient.getQueryState(queryKey)?.isInvalidated,
).toBe(true);
adapters.infrastructure.dispose();
});
it("executes installed feature HTTP through the composed contract registry", async () => {
const fetcher = vi.fn(async () =>
Response.json([{ id: "reference-1", name: "Direct contract payload" }]),
);
const adapters = await createRuntimeAdapters({
runtime,
release,
host: {},
fetcher,
});
await adapters.outputPorts.session.beginSignIn();
await vi.waitFor(() =>
expect(adapters.infrastructure.serverStateScope.getPhase()).toBe("READY"),
);
await expect(
adapters.featureInputs[REFERENCE_FEATURE_ID].listResources({ limit: 20 }),
).resolves.toEqual({
ok: true,
value: [
{
resourceId: "reference-1",
title: "Direct contract payload",
createdAt: null,
},
],
});
expect(fetcher).toHaveBeenCalledWith(
"http://localhost:8080/api/reference-resources?limit=20",
expect.objectContaining({
method: "GET",
redirect: "error",
cache: "no-store",
}),
);
adapters.infrastructure.dispose();
});
it("keeps private command intent identity out of URLs and diagnostics", async () => {
const requests: Array<Readonly<{ url: string; headers: Headers }>> = [];
const fetcher = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
requests.push({
url: String(input),
headers: new Headers(init?.headers),
});
return Response.json(
{ id: "resource-1", name: "Created resource" },
{ status: 201 },
);
});
const adapters = await createRuntimeAdapters({
runtime,
release,
host: {},
fetcher,
});
await adapters.outputPorts.session.beginSignIn();
await vi.waitFor(() =>
expect(adapters.infrastructure.serverStateScope.getPhase()).toBe("READY"),
);
const intent = Object.freeze({
intentId: "private-intent-id",
operationId: "CREATE_REFERENCE_RESOURCE",
canonicalInputIdentity: "private-canonical-input",
idempotencyKey: "private-idempotency-key",
createdAtMonotonicMs: 42,
});
await expect(
adapters.featureInputs[REFERENCE_FEATURE_ID].createResource(
{ name: "Created resource" },
{ intent },
),
).resolves.toMatchObject({ ok: true });
expect(requests).toHaveLength(1);
expect(requests[0]?.headers.get("Idempotency-Key")).toBe(
"private-idempotency-key",
);
expect(requests[0]?.url).toBe(
"http://localhost:8080/api/reference-resources",
);
const safeEvidence = JSON.stringify({
requests: requests.map((request) => request.url),
diagnostics: adapters.outputPorts.diagnostics.entries(),
});
expect(safeEvidence).not.toContain("private-intent-id");
expect(safeEvidence).not.toContain("private-canonical-input");
expect(safeEvidence).not.toContain("private-idempotency-key");
adapters.infrastructure.dispose();
});
});
@@ -0,0 +1,3 @@
import { createRuntimeAdapters } from "../../../../src/bootstrap/runtime-adapters.ts";
export const bootstrapFactory = createRuntimeAdapters;
@@ -0,0 +1,3 @@
import { createApplication } from "../../../../src/application/create-application.ts";
export const applicationFactory = createApplication;
@@ -0,0 +1,7 @@
import { createApplication } from "../../../../src/application/create-application.ts";
export const applicationFactory = createApplication;
export function AllowedPresentationFixture() {
return <p>allowed</p>;
}
@@ -0,0 +1,5 @@
import type { Value } from "../domain/value.ts";
export function readValue(): Value {
return { id: "fixture-value" };
}
@@ -0,0 +1,3 @@
export type Value = {
readonly id: string;
};
@@ -0,0 +1,5 @@
import { readValue } from "../application/read-value.ts";
export function ValueView() {
return <p>{readValue().id}</p>;
}
@@ -0,0 +1,3 @@
import { second } from "./second.ts";
export const first = `first:${second}`;
@@ -0,0 +1,3 @@
import { first } from "./first.ts";
export const second = `second:${first}`;
@@ -0,0 +1 @@
export const runtimeName = "forbidden-runtime";
@@ -0,0 +1,3 @@
import { runtimeName } from "../adapters/runtime.ts";
export const selectedRuntime = runtimeName;
@@ -0,0 +1,5 @@
const modulePath = "../domain/value.ts";
export async function loadValue() {
return import(modulePath);
}
@@ -0,0 +1,8 @@
import "/definitely-missing-architecture-fixture.ts";
import "architecture-fixture-package-that-does-not-exist";
import "file:///definitely-missing-architecture-fixture.ts";
import type { Value } from "../domain/missing-value.ts";
export function readValue(value: Value) {
return value;
}
@@ -0,0 +1,5 @@
const modulePath = "../domain/value.ts";
export function requireValue() {
return require(modulePath);
}
@@ -0,0 +1,3 @@
import { createHttpClient } from "../../../../src/adapters/http/client.ts";
export const leakedHttpFactory = createHttpClient;
@@ -0,0 +1,3 @@
import { QueryClient } from "@tanstack/react-query";
export const invalidClient = new QueryClient();
@@ -0,0 +1,3 @@
import React from "react";
export const invalidDomainValue = React.createElement("div");
@@ -0,0 +1,4 @@
export function DirectFetchPage() {
void fetch("/api/forbidden");
return <p>forbidden</p>;
}
@@ -0,0 +1,3 @@
import "../../../../src/adapters/http/client.ts";
export const invalidEdge = true;
@@ -0,0 +1,5 @@
import "../../../../src/adapters/http/client.ts";
export function InvalidPresentationFixture() {
return <p>invalid</p>;
}
@@ -0,0 +1,5 @@
import type { ApplicationOutputPorts } from "../../../../src/application/ports/out/application-output-ports.ts";
export function OutputPortLeak(_props: ApplicationOutputPorts) {
return <p>forbidden</p>;
}
@@ -0,0 +1,3 @@
import { useQuery } from "@tanstack/react-query";
export const leakedQueryHook = useQuery;
@@ -0,0 +1,5 @@
import type { ApplicationApi } from "../../../../src/application/ports/in/application-api.ts";
export function ForbiddenTemplate(_props: { application: ApplicationApi }) {
return <main>Template must not select an application use case.</main>;
}
@@ -0,0 +1,6 @@
export function createOwnedBrowserStorageCapabilities() {
return {
local: localStorage,
session: sessionStorage,
};
}
@@ -0,0 +1,6 @@
export function createOwnedCrossContextCapabilities() {
return {
channel: new BroadcastChannel("owned-cross-context-fixture"),
storage: localStorage,
};
}
@@ -0,0 +1,17 @@
import type { BrowserStoragePolicy } from "../../../../src/application/ports/browser-file-storage/shared.ts";
export const POLICY: BrowserStoragePolicy = {
owner: "fixture-owner",
namespace: "fixture",
classification: "INTERNAL",
authority: "RECONSTRUCTABLE",
accountScope: "OPAQUE_PARTITION",
retention: { kind: "TTL", maxAgeMs: 1_000 },
softBudgetBytes: 1_000,
hardBudgetBytes: 2_000,
evictionPriority: "RECONSTRUCTABLE",
logoutAction: "EXPORT_THEN_PURGE",
accountDeletionAction: "PURGE_PARTITION",
pressureAction: "RETAIN",
unavailableFallback: "ONLINE_ONLY",
};
@@ -0,0 +1,4 @@
export function bypassThroughAlias() {
const browser = globalThis;
return browser.indexedDB.open("forbidden");
}
@@ -0,0 +1,5 @@
class BrowserRootHolder {
readonly root = globalThis;
}
new BrowserRootHolder().root.indexedDB.open("forbidden");
@@ -0,0 +1,4 @@
export function bypassThroughComputedAlias() {
const browser = globalThis;
return browser["caches"].open("forbidden");
}
@@ -0,0 +1,5 @@
class BrowserRootBox {
constructor(readonly root: typeof globalThis) {}
}
new BrowserRootBox(globalThis).root.indexedDB.open("forbidden");
@@ -0,0 +1,3 @@
export function bypassThroughDefault(browser = globalThis) {
return browser.indexedDB.open("forbidden");
}
@@ -0,0 +1,3 @@
export function constructBlobOutsideOwnedAdapter() {
return new Blob(["forbidden"]);
}
@@ -0,0 +1,3 @@
export function openBroadcastChannelDirectly() {
return new BroadcastChannel("forbidden");
}
@@ -0,0 +1,3 @@
export function openCacheDirectly() {
return caches.open("forbidden");
}
@@ -0,0 +1,3 @@
export function openDatabaseDirectly() {
return indexedDB.open("forbidden");
}
@@ -0,0 +1,3 @@
export function writeLocalStorageDirectly() {
localStorage.setItem("forbidden", "value");
}
@@ -0,0 +1,3 @@
export function openOpfsDirectly() {
return navigator.storage.getDirectory();
}
@@ -0,0 +1,3 @@
export function writeSessionStorageDirectly() {
sessionStorage.setItem("forbidden", "value");
}
@@ -0,0 +1,5 @@
export function bypassThroughDynamicCapabilityKey() {
const browser = globalThis;
const capability = "indexedDB";
return browser[capability].open("forbidden");
}
@@ -0,0 +1,4 @@
export function bypassThroughObjectContainer() {
const holder = { browser: globalThis };
return holder.browser.indexedDB.open("forbidden");
}
@@ -0,0 +1,3 @@
export function bypassThroughGlobalThis() {
return globalThis.indexedDB.open("forbidden");
}
@@ -0,0 +1,7 @@
function identity<Value>(value: Value): Value {
return value;
}
export function bypassThroughIdentityWrapper() {
return identity(globalThis).indexedDB.open("forbidden");
}
@@ -0,0 +1,4 @@
const holder: { root?: typeof globalThis } = {};
holder.root = globalThis;
holder.root.indexedDB.open("forbidden");
@@ -0,0 +1,3 @@
export function allocateObjectUrlDirectly(value: never) {
return URL.createObjectURL(value);
}
@@ -0,0 +1,7 @@
export function bypassThroughPropertyDescriptor() {
const descriptor = Object.getOwnPropertyDescriptor(
globalThis,
"indexedDB",
);
return (descriptor?.value as IDBFactory).open("forbidden");
}
@@ -0,0 +1,3 @@
export function bypassThroughReflection() {
return Reflect.get(globalThis, "indexedDB");
}
+14
View File
@@ -0,0 +1,14 @@
{
"schemaVersion": 2,
"providerAdapter": ".gitea/workflows/quality-gates.yml",
"commands": [],
"artifactSchemas": [{ "id": "text", "kind": "text", "maxBytes": 1024 }],
"artifacts": [{ "id": "log", "path": "artifacts/gate.txt", "schemaId": "text", "production": "runner-generated" }],
"gates": [
{ "id": "FE-GATE-001", "name": "one", "commandIds": ["command"], "logArtifactId": "log", "evidenceArtifactIds": ["log"], "retentionClassId": "merge" },
{ "id": "FE-GATE-001", "name": "duplicate", "commandIds": ["command"], "logArtifactId": "log", "evidenceArtifactIds": ["log"], "retentionClassId": "merge" }
],
"stages": [],
"jobs": [],
"retention": { "durationStatus": "UNSUPPORTED", "classes": [{ "id": "merge", "policy": "one cycle" }] }
}
+4
View File
@@ -0,0 +1,4 @@
{
"jobId": "merge_gate",
"needs": ["release_gate"]
}
+11
View File
@@ -0,0 +1,11 @@
{
"schemaVersion": 2,
"providerAdapter": ".gitea/workflows/quality-gates.yml",
"commands": [],
"artifactSchemas": [],
"artifacts": [{ "id": "log", "path": "artifacts/gate.txt", "schemaId": "missing", "production": "runner-generated" }],
"gates": [],
"stages": [],
"jobs": [],
"retention": { "durationStatus": "UNSUPPORTED", "classes": [] }
}
+4
View File
@@ -0,0 +1,4 @@
{
"jobId": "release_gate",
"addGateId": "FE-GATE-001"
}
+12
View File
@@ -0,0 +1,12 @@
{
"schemaVersion": 2,
"providerAdapter": ".gitea/workflows/quality-gates.yml",
"commands": [],
"artifactSchemas": [],
"artifacts": [],
"gates": [],
"stages": [],
"jobs": [],
"retention": { "durationStatus": "UNSUPPORTED", "classes": [] },
"unexpected": true
}
@@ -0,0 +1,4 @@
{
"jobId": "merge_gate",
"needs": ["missing"]
}
+4
View File
@@ -0,0 +1,4 @@
{
"jobId": "merge_gate",
"removeGateId": "FE-GATE-001"
}
+14
View File
@@ -0,0 +1,14 @@
{
"total": {
"lines": { "pct": 0 },
"statements": { "pct": 0 },
"functions": { "pct": 0 },
"branches": { "pct": 0 }
},
"src/adapters/http/http-execution-v3.ts": {
"lines": { "pct": 0 },
"statements": { "pct": 0 },
"functions": { "pct": 0 },
"branches": { "pct": 0 }
}
}
+50
View File
@@ -0,0 +1,50 @@
{
"total": {
"lines": { "total": 14, "covered": 14, "skipped": 0, "pct": 100 },
"statements": { "total": 14, "covered": 14, "skipped": 0, "pct": 100 },
"functions": { "total": 14, "covered": 14, "skipped": 0, "pct": 100 },
"branches": { "total": 14, "covered": 14, "skipped": 0, "pct": 100 }
},
"src/adapters/http/bounded-body-reader.ts": {
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/adapters/http/bounded-json.ts": {
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/adapters/http/http-execution-v3.ts": {
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/adapters/http/request-builder.ts": {
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/adapters/http/retry-policy.ts": {
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/adapters/query-cache/server-state-scope-runtime.ts": {
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/adapters/service-worker/service-worker-lifecycle.ts": {
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/adapters/storage/browser-storage-adapter.ts": {
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/adapters/telemetry/best-effort-telemetry.ts": {
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/application/create-application.ts": {
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/application/policies/compatibility.ts": {
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/application/policies/performance-budgets.ts": {
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/application/policies/promotion-readiness.ts": {
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
},
"src/application/use-cases/decide-chunk-recovery.ts": {
"lines": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "statements": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "functions": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }, "branches": { "total": 1, "covered": 1, "skipped": 0, "pct": 100 }
}
}

Some files were not shown because too many files have changed in this diff Show More