persistentShell.test.tsx3.2 KBView on GitHub
import { fireEvent, render, screen } from '@testing-library/react';
import { MemoryRouter, Route, Routes, useNavigate } from 'react-router';
import React from 'react';

// Mount/unmount spies for the mocked AppShell. jest hoists jest.mock above imports, so the
// referenced variables must be prefixed with `mock` to satisfy the out-of-scope guard.
const mockMount = jest.fn();
const mockUnmount = jest.fn();

jest.mock('@/components/layout/AppShell', () => {
  const ReactMod = require('react');
  return {
    AppShell: ({ children }: { children: React.ReactNode }) => {
      ReactMod.useEffect(() => {
        mockMount();
        return () => mockUnmount();
      }, []);
      return ReactMod.createElement('div', { 'data-testid': 'app-shell' }, children);
    },
  };
});

// Import AFTER the mock so PersistentShell resolves the stubbed AppShell.
import { PersistentShell } from '@/modules/ux/layout/PersistentShell';

function Navigator() {
  const navigate = useNavigate();
  return (
    <>
      <button onClick={() => navigate('/pipeline')}>go-pipeline</button>
      <button onClick={() => navigate('/settings')}>go-settings</button>
      <button onClick={() => navigate('/mail')}>go-mail</button>
    </>
  );
}

function Harness() {
  return (
    <MemoryRouter initialEntries={['/mail']}>
      <PersistentShell>
        <Routes>
          <Route path="/mail" element={<div>mail-page</div>} />
          <Route path="/pipeline" element={<div>pipeline-page</div>} />
          <Route path="/settings" element={<div>settings-page</div>} />
        </Routes>
      </PersistentShell>
      <Navigator />
    </MemoryRouter>
  );
}

describe('PersistentShell — the persistent root shell (Phase 1)', () => {
  beforeEach(() => {
    mockMount.mockClear();
    mockUnmount.mockClear();
  });

  it('mounts AppShell on a shell route', () => {
    render(<Harness />);
    expect(screen.getByTestId('app-shell')).toBeInTheDocument();
    expect(screen.getByText('mail-page')).toBeInTheDocument();
    expect(mockMount).toHaveBeenCalledTimes(1);
  });

  it('keeps the SAME AppShell instance mounted across shell→shell switches (no remount)', () => {
    render(<Harness />);
    expect(mockMount).toHaveBeenCalledTimes(1);

    fireEvent.click(screen.getByText('go-pipeline'));
    // Routed content swapped …
    expect(screen.getByText('pipeline-page')).toBeInTheDocument();
    expect(screen.queryByText('mail-page')).not.toBeInTheDocument();
    // … but the chat shell was NOT remounted — the whole point of Phase 1.
    expect(mockMount).toHaveBeenCalledTimes(1);
    expect(mockUnmount).not.toHaveBeenCalled();
  });

  it('unmounts AppShell on a full-width route, remounts on return to a shell route', () => {
    render(<Harness />);
    expect(mockMount).toHaveBeenCalledTimes(1);

    fireEvent.click(screen.getByText('go-settings'));
    expect(screen.getByText('settings-page')).toBeInTheDocument();
    expect(screen.queryByTestId('app-shell')).not.toBeInTheDocument();
    expect(mockUnmount).toHaveBeenCalledTimes(1);

    fireEvent.click(screen.getByText('go-mail'));
    expect(screen.getByTestId('app-shell')).toBeInTheDocument();
    expect(mockMount).toHaveBeenCalledTimes(2);
  });
});