feat: execute route and release recovery contracts
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
// @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.js";
|
||||
import { FeatureBoundary } from "../../src/presentation/boundaries/render-error-boundary.jsx";
|
||||
|
||||
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("error.render_failure");
|
||||
expect(recover).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -91,4 +91,34 @@ describe("render recovery boundaries", () => {
|
||||
);
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
createAnonymousSessionAdapter,
|
||||
@@ -14,10 +14,13 @@ import { createTestApplication } from "../helpers/create-test-application.js";
|
||||
|
||||
/**
|
||||
* @param {import("../../src/application/ports/auth-session-port.js").AuthSessionPort} session
|
||||
* @param {Parameters<typeof createTestApplication>[0]} [overrides]
|
||||
*/
|
||||
function renderRouter(session) {
|
||||
function renderRouter(session, overrides = {}) {
|
||||
return render(
|
||||
<ApplicationProvider application={createTestApplication({ session })}>
|
||||
<ApplicationProvider
|
||||
application={createTestApplication({ ...overrides, session })}
|
||||
>
|
||||
<AppRouter />
|
||||
</ApplicationProvider>,
|
||||
);
|
||||
@@ -50,6 +53,10 @@ describe("application router", () => {
|
||||
await screen.findByRole("heading", { name: "UI 구성요소", level: 1 }),
|
||||
).toBeVisible();
|
||||
expect(window.location.pathname).toBe("/examples/ui");
|
||||
expect(document.title).toBe("UI 구성요소 · Frontend Skeleton");
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "UI 구성요소", level: 1 }),
|
||||
).toHaveFocus();
|
||||
});
|
||||
|
||||
it("reacts to demo sign-in and opens the protected integration route", async () => {
|
||||
@@ -82,4 +89,31 @@ describe("application router", () => {
|
||||
screen.getByRole("heading", { name: "세션이 필요합니다." }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
it("rejects invalid route search before any application query runs", async () => {
|
||||
const getCurrent = vi.fn(async () => ({
|
||||
buildId: "test-build",
|
||||
releaseId: "test-release",
|
||||
configSchemaVersion: "1",
|
||||
apiContractVersion: "1",
|
||||
assetManifestHash: "test-hash",
|
||||
routeChunks: { "route-sample-resources": "assets/sample.js" },
|
||||
}));
|
||||
window.history.pushState({}, "", "/sample/resources?limit=invalid");
|
||||
renderRouter(createDemoSessionAdapter("authenticated"), {
|
||||
releaseInfo: { getCurrent, refresh: getCurrent },
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByRole("heading", {
|
||||
name: "올바르지 않은 주소입니다.",
|
||||
}),
|
||||
).toBeVisible();
|
||||
expect(screen.getAllByRole("heading", { level: 1 })).toHaveLength(1);
|
||||
expect(screen.getByText("안전한 탐색 링크를 사용해 주세요.")).toHaveAttribute(
|
||||
"data-route-error",
|
||||
"ROUTE_SEARCH_INVALID",
|
||||
);
|
||||
expect(getCurrent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,6 +30,14 @@ const releaseManifest = {
|
||||
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-sample-resources": "assets/sample.js",
|
||||
"route-not-found": "assets/not-found.js",
|
||||
},
|
||||
};
|
||||
|
||||
describe("production runtime application tree", () => {
|
||||
|
||||
Reference in New Issue
Block a user