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
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",
);
});
});