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