inboxScopeReady.test.tsx3.7 KBView on GitHub import { renderHook } from '@testing-library/react';
/**
* `scopeReady` vs `isLoading`, which are NOT the same question.
*
* Both of `useInboxes`' queries are gated on the session. A react-query v5 query that is
* DISABLED reports `isLoading: false` — it is pending, but not fetching — so `isLoading`
* reads as "loaded" for the whole window before the session resolves, with no settings
* behind it. That is exactly the window in which `inboxLayout` falls back to `inbox`, no
* stub owns the `important` / `other` slug, and a feed asked now is asked without its
* inbox half (see feedScopeGate.test.tsx for the other end of the same bug).
*
* An ERROR settles it too: a failed settings fetch has to let the feed through rather
* than hold it behind a spinner forever.
*/
type QueryState = { data?: unknown; isLoading: boolean; isPending: boolean };
const mockSettingsState: QueryState = { data: undefined, isLoading: false, isPending: true };
const mockInboxesState: QueryState = { data: undefined, isLoading: false, isPending: true };
jest.mock('@tanstack/react-query', () => ({
// The first `useQuery` call in useInboxes is settings, the second is inboxes.
useQuery: (() => {
let call = 0;
return () => (call++ % 2 === 0 ? mockSettingsState : mockInboxesState);
})(),
useMutation: () => ({ mutate: jest.fn() }),
useQueryClient: () => ({
getQueryData: jest.fn(),
setQueryData: jest.fn(),
cancelQueries: jest.fn(),
invalidateQueries: jest.fn(),
}),
}));
jest.mock('@/modules/auth/utils/auth-client', () => ({
useSession: () => ({ data: undefined }),
}));
jest.mock('@/providers/query-provider', () => ({
useTRPC: () => ({
settings: {
get: { queryKey: () => ['settings'], queryOptions: () => ({}) },
save: { mutationOptions: () => ({}) },
},
mail: {
listInboxes: { queryKey: () => ['inboxes'], queryOptions: () => ({}) },
createInbox: { mutationOptions: () => ({}) },
updateInbox: { mutationOptions: () => ({}) },
deleteInbox: { mutationOptions: () => ({}) },
reorderInboxes: { mutationOptions: () => ({}) },
previewSplitQuery: { mutationOptions: () => ({}) },
},
}),
}));
import { useInboxes } from '@/modules/threads/hooks/use-inboxes';
const setState = (target: QueryState, next: QueryState) => Object.assign(target, next);
describe('useInboxes — scopeReady', () => {
it('is false while the session (and so the settings query) is still pending', () => {
setState(mockSettingsState, { data: undefined, isLoading: false, isPending: true });
setState(mockInboxesState, { data: undefined, isLoading: false, isPending: true });
const { result } = renderHook(() => useInboxes());
// The trap: nothing is in flight, so `isLoading` already says "done".
expect(result.current.isLoading).toBe(false);
expect(result.current.scopeReady).toBe(false);
});
it('is true once both queries have settled', () => {
setState(mockSettingsState, { data: { settings: {} }, isLoading: false, isPending: false });
setState(mockInboxesState, { data: [], isLoading: false, isPending: false });
const { result } = renderHook(() => useInboxes());
expect(result.current.scopeReady).toBe(true);
});
it('is true when a query FAILED, so a broken fetch cannot spin the feed forever', () => {
// react-query leaves `isPending` false on error — there is an answer, it is just a bad one.
setState(mockSettingsState, { data: undefined, isLoading: false, isPending: false });
setState(mockInboxesState, { data: undefined, isLoading: false, isPending: false });
const { result } = renderHook(() => useInboxes());
expect(result.current.scopeReady).toBe(true);
});
});