mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
`EntryShell` keeps every entry view mounted and hides the inactive ones with `display: none` + `inert` + `aria-hidden`. `TasksView` is therefore live from the first paint of Home, and its mount effect pulls four endpoints — the automation catalog, pending proposals, routines, and the project picker — for a tab the user has not opened. It also runs the whole set twice per launch. `refresh` is keyed on `tasksWorkspaceIdentity`, which changes when `/api/workspace/context` resolves, so the identity-change pass repeats every request the pre-context pass made. That accounts for `/api/automation-templates`, `/api/automation-proposals` and `/api/routines` each appearing twice in a cold Home load. The repository already has the answer for this: `ProjectsView` receives `isActive`, and `DesignSystemsTab` early-returns on it. `TasksView` was simply never given the prop. Add it and gate the refresh effect. `isActive` defaults to true, so the six suites that render `<TasksView />` directly — and any other caller — keep their current behaviour; only `EntryShell` passes the real value. The effect depends on `isActive` as well as `refresh`, so an identity change while hidden re-enters it, returns early, and the fetch happens when the tab is opened. This delays the work, it does not suppress it. Nothing outside the component depends on it having loaded: `TasksView` takes no callbacks and reports no counts upward, and it has a single mount site.
89 lines
3.2 KiB
TypeScript
89 lines
3.2 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { TasksView } from '../../src/components/TasksView';
|
|
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
const LAUNCH_ENDPOINTS = [
|
|
'/api/automation-templates',
|
|
'/api/automation-proposals?status=pending-review',
|
|
'/api/routines',
|
|
];
|
|
|
|
function trackedFetch(seen: string[]) {
|
|
return vi.fn(async (input: RequestInfo | URL) => {
|
|
const url = input.toString();
|
|
seen.push(url);
|
|
if (url.startsWith('/api/routines')) {
|
|
return new Response(JSON.stringify({ routines: [] }), { status: 200 });
|
|
}
|
|
if (url.startsWith('/api/automation-templates')) {
|
|
return new Response(JSON.stringify({ templates: [] }), { status: 200 });
|
|
}
|
|
if (url.startsWith('/api/automation-proposals')) {
|
|
return new Response(JSON.stringify({ proposals: [] }), { status: 200 });
|
|
}
|
|
if (url.startsWith('/api/projects') || url.includes('/projects')) {
|
|
return new Response(JSON.stringify({ projects: [] }), { status: 200 });
|
|
}
|
|
return new Response(JSON.stringify({}), { status: 200 });
|
|
}) as unknown as typeof fetch;
|
|
}
|
|
|
|
describe('TasksView inactive view', () => {
|
|
afterEach(() => {
|
|
cleanup();
|
|
globalThis.fetch = originalFetch;
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('does not fetch automation data while the view is hidden', async () => {
|
|
// `EntryShell` keeps every entry view mounted and hides the inactive ones
|
|
// with `display: none` + `inert`, so Automations loads its catalog, its
|
|
// proposals, its routines and the project picker on every Home launch —
|
|
// for a tab the user has not opened. Worse, the whole set runs twice,
|
|
// because `tasksWorkspaceIdentity` changes when `/api/workspace/context`
|
|
// resolves and `refresh` is keyed on it.
|
|
const seen: string[] = [];
|
|
globalThis.fetch = trackedFetch(seen);
|
|
|
|
render(<TasksView isActive={false} />);
|
|
|
|
// Give the mount effects a chance to run before asserting the absence.
|
|
await waitFor(() => expect(screen.getByRole('heading', { name: 'Automations' })).toBeTruthy());
|
|
for (const endpoint of LAUNCH_ENDPOINTS) {
|
|
expect(seen).not.toContain(endpoint);
|
|
}
|
|
});
|
|
|
|
it('fetches once the view becomes active', async () => {
|
|
// The gate must not turn into "never loads": opening the tab has to fill it.
|
|
const seen: string[] = [];
|
|
globalThis.fetch = trackedFetch(seen);
|
|
|
|
const { rerender } = render(<TasksView isActive={false} />);
|
|
await waitFor(() => expect(screen.getByRole('heading', { name: 'Automations' })).toBeTruthy());
|
|
expect(seen).not.toContain('/api/routines');
|
|
|
|
rerender(<TasksView isActive />);
|
|
await waitFor(() => expect(seen).toContain('/api/routines'));
|
|
for (const endpoint of LAUNCH_ENDPOINTS) {
|
|
expect(seen).toContain(endpoint);
|
|
}
|
|
});
|
|
|
|
it('still loads for callers that do not pass the flag', async () => {
|
|
// Six existing suites render `<TasksView />` bare, and so may other callers;
|
|
// the gate is opt-in, not a new requirement.
|
|
const seen: string[] = [];
|
|
globalThis.fetch = trackedFetch(seen);
|
|
|
|
render(<TasksView />);
|
|
|
|
await waitFor(() => expect(seen).toContain('/api/routines'));
|
|
});
|
|
});
|