Refactor App.tsx from god component to context-based architecture
All checks were successful
CI / check (push) Successful in 1m18s
CI / build-image (push) Has been skipped

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-19 14:19:58 +01:00
parent 6584d8d064
commit 86768842ff
35 changed files with 1065 additions and 795 deletions

View File

@@ -11,28 +11,51 @@ vi.mock("../../adapters/bestiary-cache.js", () => ({
clearAll: vi.fn(),
}));
// Mock the context module
vi.mock("../../contexts/bestiary-context.js", () => ({
useBestiaryContext: vi.fn(),
}));
import * as bestiaryCache from "../../adapters/bestiary-cache.js";
import { SourceManager } from "../source-manager";
import { useBestiaryContext } from "../../contexts/bestiary-context.js";
import { SourceManager } from "../source-manager.js";
const mockGetCachedSources = vi.mocked(bestiaryCache.getCachedSources);
const mockClearSource = vi.mocked(bestiaryCache.clearSource);
const mockClearAll = vi.mocked(bestiaryCache.clearAll);
const mockUseBestiaryContext = vi.mocked(useBestiaryContext);
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
function setupMockContext() {
const refreshCache = vi.fn().mockResolvedValue(undefined);
mockUseBestiaryContext.mockReturnValue({
refreshCache,
search: vi.fn().mockReturnValue([]),
getCreature: vi.fn(),
isLoaded: true,
isSourceCached: vi.fn().mockResolvedValue(false),
fetchAndCacheSource: vi.fn(),
uploadAndCacheSource: vi.fn(),
} as ReturnType<typeof useBestiaryContext>);
return { refreshCache };
}
describe("SourceManager", () => {
it("shows 'No cached sources' empty state when no sources", async () => {
setupMockContext();
mockGetCachedSources.mockResolvedValue([]);
render(<SourceManager onCacheCleared={vi.fn()} />);
render(<SourceManager />);
await waitFor(() => {
expect(screen.getByText("No cached sources")).toBeInTheDocument();
});
});
it("lists cached sources with display name and creature count", async () => {
setupMockContext();
mockGetCachedSources.mockResolvedValue([
{
sourceCode: "mm",
@@ -47,7 +70,7 @@ describe("SourceManager", () => {
cachedAt: Date.now(),
},
]);
render(<SourceManager onCacheCleared={vi.fn()} />);
render(<SourceManager />);
await waitFor(() => {
expect(screen.getByText("Monster Manual")).toBeInTheDocument();
});
@@ -56,9 +79,9 @@ describe("SourceManager", () => {
expect(screen.getByText("100 creatures")).toBeInTheDocument();
});
it("Clear All button calls cache clear and onCacheCleared", async () => {
it("Clear All button calls cache clear and refreshCache", async () => {
const user = userEvent.setup();
const onCacheCleared = vi.fn();
const { refreshCache } = setupMockContext();
mockGetCachedSources
.mockResolvedValueOnce([
{
@@ -70,7 +93,7 @@ describe("SourceManager", () => {
])
.mockResolvedValue([]);
mockClearAll.mockResolvedValue(undefined);
render(<SourceManager onCacheCleared={onCacheCleared} />);
render(<SourceManager />);
await waitFor(() => {
expect(screen.getByText("Monster Manual")).toBeInTheDocument();
@@ -80,12 +103,12 @@ describe("SourceManager", () => {
await waitFor(() => {
expect(mockClearAll).toHaveBeenCalled();
});
expect(onCacheCleared).toHaveBeenCalled();
expect(refreshCache).toHaveBeenCalled();
});
it("individual source delete button calls clear for that source", async () => {
const user = userEvent.setup();
const onCacheCleared = vi.fn();
const { refreshCache } = setupMockContext();
mockGetCachedSources
.mockResolvedValueOnce([
{
@@ -111,7 +134,7 @@ describe("SourceManager", () => {
]);
mockClearSource.mockResolvedValue(undefined);
render(<SourceManager onCacheCleared={onCacheCleared} />);
render(<SourceManager />);
await waitFor(() => {
expect(screen.getByText("Monster Manual")).toBeInTheDocument();
});
@@ -122,6 +145,6 @@ describe("SourceManager", () => {
await waitFor(() => {
expect(mockClearSource).toHaveBeenCalledWith("mm");
});
expect(onCacheCleared).toHaveBeenCalled();
expect(refreshCache).toHaveBeenCalled();
});
});