inbox-landing-redirect.test.tsx4.7 KBView on GitHub
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { MemoryRouter, Route, Routes, useLocation } from 'react-router';

/**
 * `/mail` and `/inbox` land on the literal `inbox` slug. In the important/other
 * layout nothing owns that slug, so the folder page has to forward to the first
 * tab — otherwise the tab strip highlights Important while the thread query and
 * the omni-channel feed still run the unscoped `inbox` folder.
 *
 * Everything the jest.mock factories below touch is `mock`-prefixed and declared
 * as a hoisted function: the factories are lifted above this file's imports.
 */

type MockInbox = {
  id: string;
  name: string;
  position: number;
  system?: boolean;
  rule: { kind: 'all' };
};

function mockGetInboxSlug(inbox: { id: string; name: string }): string {
  if (inbox.id === 'default') return 'inbox';
  if (inbox.id === 'important') return 'important';
  if (inbox.id === 'other') return 'other';
  return inbox.name.toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
}

function mockGetRowInboxOrder(inboxes: MockInbox[]): MockInbox[] {
  const important = inboxes.find((i) => i.id === 'important');
  const other = inboxes.find((i) => i.id === 'other');
  const middle = inboxes.filter((i) => i.id !== 'important' && i.id !== 'other');
  if (important && other) return [important, ...middle, other];
  if (important) return [important, ...middle];
  if (other) return [...middle, other];
  return middle;
}

function mockInboxesForLayout(layout: string): MockInbox[] {
  if (layout === 'important_other') {
    return [
      { id: 'important', name: 'Important', position: 0, system: true, rule: { kind: 'all' } },
      { id: 'other', name: 'Other', position: 1, system: true, rule: { kind: 'all' } },
    ];
  }
  return [{ id: 'default', name: 'Inbox', position: 0, system: true, rule: { kind: 'all' } }];
}

let mockLayout: 'inbox' | 'important_other' | 'stacked' = 'important_other';
let mockFolder = 'inbox';

jest.mock('react-router', () => ({
  ...jest.requireActual('react-router'),
  useLoaderData: () => ({ folder: mockFolder }),
}));

jest.mock('@/modules/threads/hooks/use-inboxes', () => ({
  getInboxSlug: mockGetInboxSlug,
  getRowInboxOrder: mockGetRowInboxOrder,
  findInboxBySlug: (inboxes: MockInbox[], slug: string) =>
    inboxes.find((i) => mockGetInboxSlug(i) === slug),
  useInboxes: () => ({
    inboxes: mockInboxesForLayout(mockLayout),
    inboxLayout: mockLayout,
    hasCustomInboxOrder: false,
    activeInboxId: mockInboxesForLayout(mockLayout)[0].id,
    setActiveInboxId: jest.fn(),
    isLoading: false,
  }),
}));

jest.mock('@/modules/labels/hooks/use-labels', () => ({
  useLabels: () => ({ userLabels: [], isLoading: false }),
}));

jest.mock('@/modules/threads/mail', () => ({
  MailLayout: () => null,
}));

jest.mock('@/lib/performance-logger', () => ({
  perfLogger: { start: jest.fn(), end: jest.fn() },
}));

import MailPage from '@/app/(routes)/mail/[folder]/page';

function LocationProbe() {
  const { pathname, search } = useLocation();
  return <div data-testid="location">{`${pathname}${search}`}</div>;
}

async function landsOn(entry: string) {
  render(
    <MemoryRouter initialEntries={[entry]}>
      <LocationProbe />
      <Routes>
        <Route path="/:base/:folder" element={<MailPage />} />
      </Routes>
    </MemoryRouter>,
  );
  // The forward happens in an effect; let the navigation settle before reading.
  await waitFor(() => expect(screen.getByTestId('location')).toBeInTheDocument());
  return screen.getByTestId('location').textContent;
}

describe('inbox landing redirect', () => {
  beforeEach(() => {
    mockFolder = 'inbox';
  });

  it('forwards /mail/inbox to the first tab in the important/other layout', async () => {
    mockLayout = 'important_other';
    expect(await landsOn('/mail/inbox')).toBe('/mail/important');
  });

  it('carries the query string through so ?channel survives', async () => {
    mockLayout = 'important_other';
    expect(await landsOn('/mail/inbox?channel=all')).toBe('/mail/important?channel=all');
  });

  it('forwards on the omni-channel /inbox prefix too', async () => {
    mockLayout = 'important_other';
    expect(await landsOn('/inbox/inbox')).toBe('/inbox/important');
  });

  it('stays put when an inbox owns the `inbox` slug', async () => {
    mockLayout = 'inbox';
    expect(await landsOn('/mail/inbox')).toBe('/mail/inbox');
  });

  it('stays put in the stacked layout, whose single tab is /mail/inbox', async () => {
    mockLayout = 'stacked';
    expect(await landsOn('/mail/inbox')).toBe('/mail/inbox');
  });

  it('leaves a non-inbox folder alone', async () => {
    mockLayout = 'important_other';
    mockFolder = 'other';
    expect(await landsOn('/mail/other')).toBe('/mail/other');
  });
});