fix: synchronize TechLog focus and not-found runtime
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { act, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { lazy, type ComponentType } from "react";
|
||||
import {
|
||||
createMemoryRouter,
|
||||
Outlet,
|
||||
@@ -47,9 +48,15 @@ const routeComponents = {
|
||||
|
||||
type DiscoveryRouteId = keyof typeof routeComponents;
|
||||
|
||||
type DiscoveryRenderOptions = Readonly<{
|
||||
NotFoundComponent?: ComponentType;
|
||||
application?: ReturnType<typeof createTestApplication>;
|
||||
}>;
|
||||
|
||||
function renderDiscoveryRoute<RouteId extends DiscoveryRouteId>(
|
||||
routeId: RouteId,
|
||||
initialEntry: string,
|
||||
options: DiscoveryRenderOptions = {},
|
||||
) {
|
||||
const definition = TECH_LOG_ROUTE_REGISTRY[routeId];
|
||||
const runtime = TECH_LOG_ROUTE_RUNTIME_CONTRACT[routeId];
|
||||
@@ -67,7 +74,7 @@ function renderDiscoveryRoute<RouteId extends DiscoveryRouteId>(
|
||||
},
|
||||
NOT_FOUND: {
|
||||
moduleId: TECH_LOG_ROUTE_RUNTIME_CONTRACT.NOT_FOUND.moduleId,
|
||||
Component: NotFoundPage,
|
||||
Component: options.NotFoundComponent ?? NotFoundPage,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -86,9 +93,12 @@ function renderDiscoveryRoute<RouteId extends DiscoveryRouteId>(
|
||||
const techLog = createTechLogFeatureInstalledInput().input;
|
||||
const view = render(
|
||||
<ApplicationProvider
|
||||
application={createTestApplication({
|
||||
featureInputs: { "tech-log": techLog },
|
||||
})}
|
||||
application={
|
||||
options.application ??
|
||||
createTestApplication({
|
||||
featureInputs: { "tech-log": techLog },
|
||||
})
|
||||
}
|
||||
>
|
||||
<RouterProvider router={router} />
|
||||
</ApplicationProvider>,
|
||||
@@ -183,6 +193,62 @@ describe("TechLog home discovery", () => {
|
||||
expect(screen.getAllByRole("tabpanel")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("treats external focus URL changes as authoritative without stealing focus", async () => {
|
||||
const { router } = renderDiscoveryRoute(
|
||||
"TECH_LOG_HOME",
|
||||
"/?focus=current&state=latest-empty#latest",
|
||||
);
|
||||
const current = screen.getByRole("tab", { name: "현재 작업" });
|
||||
current.focus();
|
||||
|
||||
await router.navigate("/?focus=question&state=latest-empty#latest");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("tab", { name: "열린 질문" })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
});
|
||||
expect(router.state.location).toMatchObject({
|
||||
pathname: "/",
|
||||
search: "?focus=question&state=latest-empty",
|
||||
hash: "#latest",
|
||||
});
|
||||
expect(screen.getByRole("tab", { name: "열린 질문" })).toHaveAttribute(
|
||||
"tabindex",
|
||||
"0",
|
||||
);
|
||||
expect(current).toHaveAttribute("aria-selected", "false");
|
||||
expect(current).toHaveAttribute("tabindex", "-1");
|
||||
expect(current).toHaveFocus();
|
||||
expect(
|
||||
screen.getByRole("tabpanel", { name: "열린 질문" }),
|
||||
).toBeVisible();
|
||||
expect(document.getElementById("focus-panel-current")).toHaveAttribute(
|
||||
"hidden",
|
||||
);
|
||||
|
||||
await router.navigate("/?focus=decision&state=latest-empty#latest");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("tab", { name: "최근 결정" })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
});
|
||||
expect(router.state.location).toMatchObject({
|
||||
pathname: "/",
|
||||
search: "?focus=decision&state=latest-empty",
|
||||
hash: "#latest",
|
||||
});
|
||||
expect(screen.getByRole("tab", { name: "최근 결정" })).toHaveAttribute(
|
||||
"tabindex",
|
||||
"0",
|
||||
);
|
||||
expect(screen.getByRole("tabpanel", { name: "최근 결정" })).toBeVisible();
|
||||
expect(screen.getAllByRole("tabpanel")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("preserves the source home empty and error states", () => {
|
||||
const emptyView = renderDiscoveryRoute("TECH_LOG_HOME", "/?state=latest-empty");
|
||||
expect(screen.getByText("아직 공개된 기록이 없습니다.")).toHaveClass(
|
||||
@@ -321,6 +387,86 @@ describe("TechLog explore discovery", () => {
|
||||
).not.toBeInTheDocument();
|
||||
expect(container.querySelector(".site-frame")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("recovers a rejecting registered not-found runtime under its own chunk contract", async () => {
|
||||
let rejectNotFound: ((reason?: unknown) => void) | undefined;
|
||||
const LazyNotFound = lazy(
|
||||
() =>
|
||||
new Promise<{ default: typeof NotFoundPage }>((_resolve, reject) => {
|
||||
rejectNotFound = reject;
|
||||
}),
|
||||
);
|
||||
const notFoundChunkRead = vi.fn(() => "assets/not-found.js");
|
||||
const exploreKindChunkRead = vi.fn(() => "assets/explore-kind.js");
|
||||
const routeChunks = {
|
||||
get "route-not-found"() {
|
||||
return notFoundChunkRead();
|
||||
},
|
||||
get "route-tech-log-explore-kind"() {
|
||||
return exploreKindChunkRead();
|
||||
},
|
||||
};
|
||||
const release = {
|
||||
buildId: "task-7-test-build",
|
||||
releaseId: "task-7-test-release",
|
||||
configSchemaVersion: "1",
|
||||
apiContractVersion: "1",
|
||||
assetManifestHash: "task-7-test-hash",
|
||||
routeChunks,
|
||||
};
|
||||
const record = vi.fn();
|
||||
const techLog = createTechLogFeatureInstalledInput().input;
|
||||
const application = createTestApplication({
|
||||
diagnostics: { record },
|
||||
releaseInfo: {
|
||||
getCurrent: async () => release,
|
||||
refresh: async () => release,
|
||||
},
|
||||
featureInputs: { "tech-log": techLog },
|
||||
});
|
||||
const { router, container } = renderDiscoveryRoute(
|
||||
"TECH_LOG_EXPLORE_KIND",
|
||||
"/explore/unknown",
|
||||
{ NotFoundComponent: LazyNotFound, application },
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText("화면을 준비하고 있습니다.").closest("section"),
|
||||
).toHaveAttribute("data-surface", "none");
|
||||
await waitFor(() => expect(rejectNotFound).toBeTypeOf("function"));
|
||||
await act(async () => {
|
||||
rejectNotFound?.(
|
||||
new TypeError("Failed to fetch dynamically imported module"),
|
||||
);
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByRole("heading", {
|
||||
name: "화면 자산을 복구하지 못했습니다.",
|
||||
}),
|
||||
).toBeVisible();
|
||||
expect(notFoundChunkRead).toHaveBeenCalledOnce();
|
||||
expect(exploreKindChunkRead).not.toHaveBeenCalled();
|
||||
expect(router.state.location.pathname).toBe("/explore/unknown");
|
||||
expect(container.querySelector(".site-frame")).not.toBeNull();
|
||||
expect(
|
||||
screen.queryByRole("heading", { name: "화면을 표시하지 못했습니다." }),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
const routeChangedRecords = record.mock.calls
|
||||
.map(([entry]) => entry)
|
||||
.filter((entry) => entry.eventId === "route.changed");
|
||||
expect(routeChangedRecords).toEqual([
|
||||
expect.objectContaining({
|
||||
context: expect.objectContaining({
|
||||
route_id: "TECH_LOG_EXPLORE_KIND",
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
expect(
|
||||
record.mock.calls.some(([entry]) => entry.eventId === "ui.render.failed"),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TechLog search discovery", () => {
|
||||
|
||||
Reference in New Issue
Block a user