A page used to be listed three times (lib/modules for its label, icon and gate; shell/moduleRegistry for its page and rail; lib/pageMeta for its title), and App held every rail's layout and collapse flag. Now each module's `module.ts` says it all — label, icon, gate, its lazy page and rail, its width — handed out through its index.ts, and features/navigation/moduleRegistry lists them in nav order for the rail, the palette, the tab title, the reachable set and the shell's render. A contract test holds the registry to the page ids and each definition to its own folder, with no static import of a page. Every rail is lazy now, loaded together with its page so a pinned rail never arrives after it and shoves the content aside; the feed's and the playlists' rails took @dnd-kit off the boot path (boot 428.5 → 393.9 KiB gzip, the App chunk 122 → 32 KiB). A rail reads its own layout and collapse flag (lib/useRail) and writes a layout change to the `me` cache too, since it now mounts anew on each visit. App's session effects, its one-shot entry-URL handlers and the admin nudge move into shell hooks; App is layout. The registry's three layer suppressions are gone: none is left.
47 lines
1.9 KiB
TypeScript
47 lines
1.9 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// A rail mounts anew on each visit to its page and seeds its layout from the `me` cache. When the shell
|
|
// held the layout, a change lived on in its state; now the rail must leave the change where its next
|
|
// mount looks, or a visit to another page and back brings the server's older layout back.
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { renderHook, act } from "@testing-library/react";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import type { ReactNode } from "react";
|
|
import { api } from "./api";
|
|
import { qk } from "./queryKeys";
|
|
import { usePanelLayout } from "./useRail";
|
|
|
|
vi.mock("react-i18next", async (importOriginal) => ({
|
|
...(await importOriginal<typeof import("react-i18next")>()),
|
|
useTranslation: () => ({ t: (k: string) => k }),
|
|
}));
|
|
|
|
const OLD = {
|
|
order: ["year", "date", "channel", "tags", "language", "topic"],
|
|
collapsed: {},
|
|
hidden: {},
|
|
};
|
|
const NEW = {
|
|
order: ["tags", "date", "year", "channel", "language", "topic"],
|
|
collapsed: {},
|
|
hidden: {},
|
|
};
|
|
|
|
describe("a rail's layout", () => {
|
|
it("is the changed one on the next visit, not the server's older copy", () => {
|
|
vi.spyOn(api, "savePrefs").mockResolvedValue(undefined as never);
|
|
const qc = new QueryClient();
|
|
qc.setQueryData(qk.me(), { id: 1, preferences: { sidebarLayout: OLD } });
|
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
<QueryClientProvider client={qc}>{children}</QueryClientProvider>
|
|
);
|
|
const first = renderHook(() => usePanelLayout("feed"), { wrapper });
|
|
expect(first.result.current[0].order).toEqual(OLD.order);
|
|
act(() => first.result.current[1](NEW));
|
|
first.unmount();
|
|
const next = renderHook(() => usePanelLayout("feed"), { wrapper });
|
|
expect(next.result.current[0].order).toEqual(NEW.order);
|
|
expect(api.savePrefs).toHaveBeenCalledWith({ sidebarLayout: NEW });
|
|
});
|
|
});
|