lib/create-context-subscriptions.test.ts
Rama main · réplica de lectura del repositorio original.
// @vitest-environment jsdom
import { act, createElement, memo } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { Project } from "./types";
import { rememberCreateProject } from "./last-create-project";
const state = vi.hoisted(() => ({
pathname: "/all",
projects: [] as Project[],
issue: null as Record<string, unknown> | null,
objective: null as Record<string, unknown> | null,
dynamicCount: 0,
record: vi.fn(),
client: {},
preload: vi.fn(),
hidden: new Set<string>(),
}));
vi.mock("next/navigation", () => ({ usePathname: () => state.pathname }));
vi.mock("next/dynamic", () => ({ default: () => {
const issue = state.dynamicCount++ === 0;
return (props: Record<string, unknown>) => { if (issue) state.issue = props; else state.objective = props; return null; };
} }));
vi.mock("sonner", async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
toast: { error: vi.fn(), info: vi.fn() },
}));
vi.mock("next-intl", () => ({ useTranslations: () => (key: string) => key }));
vi.mock("@tanstack/react-query", () => ({ useQueryClient: () => state.client }));
vi.mock("@/lib/projects-context", () => ({ useProjects: () => ({ projects: state.projects }) }));
vi.mock("@/lib/auth-context", () => ({ useAuth: () => ({ user: null }) }));
vi.mock("@/lib/use-hidden-projects", () => ({ useHiddenProjects: () => ({ hiddenProjectIds: state.hidden }) }));
vi.mock("@/lib/undo/undo-context", () => ({ useUndoHistory: () => ({ record: state.record }) }));
vi.mock("@/lib/use-members-query", () => ({ useMembersQuery: () => ({ members: [] }) }));
vi.mock("@/lib/use-categories-query", () => ({ useCategoriesQuery: () => ({ categories: [] }) }));
vi.mock("@/lib/use-objectives-query", () => ({ useObjectivesQuery: () => ({ objectives: [] }) }));
vi.mock("@/lib/lazy-app-surfaces", () => ({ loadCreateIssueDialog: vi.fn(), loadObjectiveDialog: vi.fn(), preloadSurface: (...args: unknown[]) => state.preload(...args) }));
import { CreateProvider, useCreate } from "./create-context";
beforeEach(() => {
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
state.pathname = "/all";
state.projects = [{ id: "first" }, { id: "second" }] as Project[];
state.issue = null;
state.objective = null;
state.hidden = new Set();
window.localStorage.clear();
window.history.replaceState(null, "", "/all");
});
afterEach(() => { vi.unstubAllGlobals(); vi.restoreAllMocks(); vi.clearAllMocks(); });
describe("creation action subscriptions", () => {
it("keeps 600 closed picker consumers asleep while using the latest route and explicit overrides", async () => {
let actions: ReturnType<typeof useCreate> | null = null;
const renders = vi.fn();
const Consumer = memo(() => { actions = useCreate(); renders(); return null; });
const children = Array.from({ length: 600 }, (_, id) => createElement(Consumer, { key: id }));
const root = createRoot(document.createElement("div"));
const render = () => root.render(createElement(CreateProvider, { children }));
const route = async (href: string) => {
state.pathname = new URL(href, "http://localhost").pathname;
window.history.replaceState(null, "", href);
await act(render);
};
try {
await act(render);
const original = actions!;
expect(renders).toHaveBeenCalledTimes(600);
await route("/projects/first/pages/one");
await route("/projects/first/pages/two");
await route("/projects/second?objective=goal");
expect(actions).toBe(original);
expect(renders).toHaveBeenCalledTimes(600);
await act(() => original.openCreateIssue({ status: "todo" }));
expect(state.issue).toMatchObject({ projectId: "second", initialObjectiveId: "goal", initialStatus: "todo", open: true });
await act(() => original.openCreateIssue({ projectId: "first", objectiveId: null, dictate: true }));
expect(state.issue).toMatchObject({ projectId: "first", initialObjectiveId: null, autoDictate: true });
await act(() => original.openCreateIssue({ projectId: "first" }));
expect(state.issue).toMatchObject({ projectId: "first", initialObjectiveId: null, autoDictate: false });
await act(() => original.openCreateObjective({ name: " New goal " }));
expect(state.objective).toMatchObject({ projectId: "second", initialName: "New goal", open: true });
await route("/projects/first/pages/three");
await act(() => original.warmCreateIssue());
expect(state.issue).toMatchObject({ projectId: "first" });
await route("/projects/second/pages/four");
await act(() => original.warmCreateObjective());
expect(state.objective).toMatchObject({ projectId: "second" });
expect(actions).toBe(original);
expect(renders).toHaveBeenCalledTimes(600);
} finally { await act(() => root.unmount()); }
});
it("uses current project access for remembered targets and reports availability without stale closures", async () => {
let actions: ReturnType<typeof useCreate> | null = null;
const Consumer = memo(() => { actions = useCreate(); return null; });
const children = createElement(Consumer);
const root = createRoot(document.createElement("div"));
const render = () => root.render(createElement(CreateProvider, { children }));
try {
await act(render);
const original = actions!;
rememberCreateProject("second");
await act(() => original.openCreateIssue());
expect(state.issue).toMatchObject({ projectId: "second" });
state.projects = [{ id: "third" }, { id: "fourth" }] as Project[];
await act(render);
expect(actions).toBe(original);
await act(() => original.openCreateIssue());
expect(state.issue).toMatchObject({ projectId: "third" });
vi.spyOn(Storage.prototype, "getItem").mockImplementation(() => { throw new Error("Storage unavailable"); });
await act(() => original.openCreateObjective());
expect(state.objective).toMatchObject({ projectId: "third" });
state.projects = [];
await act(render);
expect(actions!.canCreate).toBe(false);
const previousIssue = state.issue;
const previousObjective = state.objective;
await act(() => { original.openCreateIssue(); original.openCreateObjective(); });
expect(state.issue).toBe(previousIssue);
expect(state.objective).toBe(previousObjective);
} finally { await act(() => root.unmount()); }
});
it("neither offers nor defaults to a hidden project, except the one on screen (MIN-751)", async () => {
let actions: ReturnType<typeof useCreate> | null = null;
const Consumer = memo(() => { actions = useCreate(); return null; });
const root = createRoot(document.createElement("div"));
const render = () => root.render(createElement(CreateProvider, { children: createElement(Consumer) }));
const pickerIds = () => (state.issue!.projects as Project[]).map((p) => p.id);
state.hidden = new Set(["second"]);
try {
await act(render);
rememberCreateProject("second");
await act(() => actions!.openCreateIssue());
expect(state.issue).toMatchObject({ projectId: "first" });
expect(pickerIds()).toEqual(["first"]);
// Opened by link, the hidden project is where I create, and it is listed.
state.pathname = "/projects/second";
await act(render);
await act(() => actions!.openCreateIssue());
expect(state.issue).toMatchObject({ projectId: "second" });
expect(pickerIds()).toEqual(["first", "second"]);
} finally { await act(() => root.unmount()); }
});
});