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);
  });
});