// @vitest-environment jsdom
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import type { ReactNode } from "react";
import { MemoryRouter, useLocation, useNavigate } from "react-router-dom";
import { afterEach, describe, expect, it, vi } from "vitest";
import { createTechLogFeatureInstalledInput } from "../../../src/features/tech-log/adapters/create-tech-log-feature-input.ts";
import { MOCK_STUDIO_INSTALL_CONTEXT } from "../../helpers/studio-install-context.ts";
import type { StudioGateway } from "../../../src/features/tech-log/application/ports/studio-gateway.ts";
import type { DocumentPage } from "../../../src/features/tech-log/contracts/studio/contract.ts";
import { DocumentList } from "../../../src/features/tech-log/presentation/studio/components/document-list.tsx";
import { NewDocumentForm } from "../../../src/features/tech-log/presentation/studio/components/new-document-form.tsx";
import { StudioDashboard } from "../../../src/features/tech-log/presentation/studio/components/studio-dashboard.tsx";
import { StudioProvider } from "../../../src/features/tech-log/presentation/studio/studio-provider.tsx";
import { ApplicationProvider } from "../../../src/presentation/providers/application-provider.tsx";
import { createTestApplication } from "../../helpers/create-test-application.ts";
afterEach(() => vi.restoreAllMocks());
function LocationProbe() {
return ;
}
function RouterStudioProvider({
children,
gateway,
}: Readonly<{ children: ReactNode; gateway: StudioGateway }>) {
const navigate = useNavigate();
return (
gateway}
navigate={(href) => {
void navigate(href);
}}
>
{children}
);
}
function renderScreen(children: ReactNode, gateway: StudioGateway) {
const input = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input;
return render(
gateway } },
})}
>
{children}
,
);
}
describe("TechLog Studio index screens", () => {
it("shows the source dashboard totals, workflow sections, status links, and sample rows", async () => {
const gateway = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway();
const { container } = renderScreen(, gateway);
const summary = await screen.findByLabelText("Studio 요약");
expect(summary).toHaveTextContent("전체 작업본7");
expect(summary).toHaveTextContent("검증할 기록5");
expect(summary).toHaveTextContent("게시 준비0");
expect(summary).toHaveTextContent("게시 기록4");
expect(screen.getByRole("heading", { level: 1, name: "작업 흐름" })).toBeVisible();
expect(
Array.from(container.querySelectorAll(".studio-section-title a"), (link) => [
link.textContent,
link.getAttribute("href"),
]),
).toEqual([
["전체 보기", "/studio/documents"],
["전체 보기", "/studio/documents"],
["전체 보기", "/studio/documents"],
["게시 기록 보기", "/studio/publications"],
]);
expect(screen.getAllByText("컬렉션 Fetch Join과 페이징은 왜 충돌하는가").length).toBeGreaterThan(0);
});
it("searches and filters documents and renders the source empty state", async () => {
const gateway = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway();
renderScreen(, gateway);
expect(await screen.findByText("7개의 작업본")).toBeVisible();
fireEvent.change(screen.getByLabelText("검색"), { target: { value: "Fetch Join" } });
fireEvent.submit(screen.getByRole("search"));
expect(await screen.findByText("1개의 작업본")).toBeVisible();
expect(screen.getByText("컬렉션 Fetch Join과 페이징은 왜 충돌하는가")).toBeVisible();
fireEvent.change(screen.getByLabelText("종류"), { target: { value: "QUESTION" } });
expect(await screen.findByText("0개의 작업본")).toBeVisible();
expect(screen.getByRole("heading", { level: 2, name: "조건에 맞는 작업본이 없습니다" })).toBeVisible();
});
it("cancels an obsolete list request and advances cursor pagination", async () => {
const base = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway();
const all = await base.listDocuments({ limit: 20 });
let obsoleteSignal: AbortSignal | undefined;
const firstPage: DocumentPage = { items: all.items.slice(0, 1), nextCursor: "next-page" };
const secondPage: DocumentPage = { items: all.items.slice(1, 2), nextCursor: null };
const listDocuments = vi.fn()
.mockImplementationOnce((_query, { signal } = {}) => {
obsoleteSignal = signal;
return new Promise(() => {});
})
.mockResolvedValueOnce(firstPage)
.mockResolvedValueOnce(secondPage);
const gateway = { ...base, listDocuments } satisfies StudioGateway;
renderScreen(, gateway);
fireEvent.change(screen.getByLabelText("종류"), { target: { value: "CASE" } });
await waitFor(() => expect(obsoleteSignal?.aborted).toBe(true));
expect(await screen.findByText("1개의 작업본")).toBeVisible();
await userEvent.click(screen.getByRole("button", { name: "다음 작업본" }));
await waitFor(() => expect(listDocuments).toHaveBeenCalledTimes(3));
expect(screen.getByText(secondPage.items[0]!.title)).toBeVisible();
expect(screen.queryByRole("button", { name: "다음 작업본" })).not.toBeInTheDocument();
});
it("shows a retry action after a list failure and recovers", async () => {
const user = userEvent.setup();
const base = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway();
const recovered = await base.listDocuments({ limit: 20 });
const gateway = {
...base,
listDocuments: vi.fn()
.mockRejectedValueOnce(new Error("offline"))
.mockResolvedValueOnce(recovered),
} satisfies StudioGateway;
renderScreen(, gateway);
expect(await screen.findByRole("alert")).toHaveTextContent("작업본을 불러오지 못했습니다.");
await user.click(screen.getByRole("button", { name: "다시 시도" }));
expect(await screen.findByText("7개의 작업본")).toBeVisible();
expect(gateway.listDocuments).toHaveBeenCalledTimes(2);
});
it("creates the selected Question in the same session and redirects to its editor", async () => {
const user = userEvent.setup();
const gateway = createTechLogFeatureInstalledInput(MOCK_STUDIO_INSTALL_CONTEXT).input.createStudioGateway();
renderScreen(
<>
>,
gateway,
);
await user.click(screen.getByRole("radio", { name: /Question/ }));
await user.click(screen.getByRole("button", { name: "작업본 만들기" }));
await waitFor(() => expect(screen.getByLabelText("현재 경로")).toHaveTextContent(/^\/studio\/documents\/[0-9a-f-]+\/edit$/));
expect((await gateway.listDocuments({ kind: "QUESTION" })).items).toHaveLength(2);
});
});