Files
openhuman/app/src/test/test-utils.tsx
T

122 lines
4.6 KiB
TypeScript

/**
* Test utilities — provides a renderWithProviders helper that wraps
* components in a fresh Redux store + MemoryRouter for isolated testing.
*/
import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { render, type RenderOptions } from '@testing-library/react';
import type { PropsWithChildren, ReactElement } from 'react';
import { createPortal } from 'react-dom';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { SidebarSlotOutlet, SidebarSlotProvider } from '../components/layout/shell/SidebarSlot';
import { getCoreStateSnapshot } from '../lib/coreState/store';
import { CoreStateContext } from '../providers/coreStateContext';
import accountsReducer from '../store/accountsSlice';
import backendMeetReducer from '../store/backendMeetSlice';
import channelConnectionsReducer from '../store/channelConnectionsSlice';
import companionReducer from '../store/companionSlice';
import connectivityReducer from '../store/connectivitySlice';
import coreModeReducer from '../store/coreModeSlice';
import layoutReducer from '../store/layoutSlice';
import localeReducer from '../store/localeSlice';
import mascotReducer from '../store/mascotSlice';
import notificationReducer from '../store/notificationSlice';
import personaReducer from '../store/personaSlice';
import { pttReducer } from '../store/pttSlice';
import socketReducer from '../store/socketSlice';
import themeReducer from '../store/themeSlice';
import threadReducer from '../store/threadSlice';
/**
* Creates a fresh Redux store for testing.
* Uses raw (non-persisted) reducers to avoid persist complexity in tests.
*
* `mascot` is wired in for the mascot voice picker (issue #1762): the
* VoicePanel reads + dispatches against this slice, and useSelector
* would throw on a missing reducer without a stub here. `persona` is wired
* in for the same reason (issue #2345): PersonaPanel reads + dispatches
* against it. `backendMeet` is wired in for MeetingBotsCard which reads
* meeting status from this slice.
*/
const testRootReducer = combineReducers({
accounts: accountsReducer,
backendMeet: backendMeetReducer,
channelConnections: channelConnectionsReducer,
companion: companionReducer,
connectivity: connectivityReducer,
coreMode: coreModeReducer,
layout: layoutReducer,
locale: localeReducer,
mascot: mascotReducer,
notifications: notificationReducer,
persona: personaReducer,
ptt: pttReducer,
socket: socketReducer,
theme: themeReducer,
thread: threadReducer,
});
export function createTestStore(preloadedState?: Record<string, unknown>) {
return configureStore({ reducer: testRootReducer, preloadedState: preloadedState as never });
}
type TestStore = ReturnType<typeof createTestStore>;
interface ExtendedRenderOptions extends Omit<RenderOptions, 'queries'> {
preloadedState?: Record<string, unknown>;
store?: TestStore;
initialEntries?: string[];
}
/**
* Render a component wrapped in Redux Provider + MemoryRouter.
*/
export function renderWithProviders(
ui: ReactElement,
{
preloadedState,
store = createTestStore(preloadedState),
initialEntries = ['/'],
...renderOptions
}: ExtendedRenderOptions = {}
) {
const coreStateStub = {
...getCoreStateSnapshot(),
refresh: async () => {},
refreshTeams: async () => {},
refreshTeamMembers: async () => {},
refreshTeamInvites: async () => {},
setAnalyticsEnabled: async () => {},
setMeetAutoOrchestratorHandoff: async () => {},
setOnboardingCompletedFlag: async () => {},
setEncryptionKey: async () => {},
patchSnapshot: () => {},
setOnboardingTasks: async () => {},
storeSessionToken: async () => {},
clearSession: async () => {},
};
function Wrapper({ children }: PropsWithChildren) {
return (
<Provider store={store}>
<CoreStateContext.Provider value={coreStateStub}>
<MemoryRouter initialEntries={initialEntries}>
{/* Provide the root sidebar slot so pages that project their nav via
SidebarContent (Settings/Brain/Connections/Chat) render it. The
outlet is portaled to document.body so it doesn't become the
render container's firstChild (which would break tests asserting
a null/empty render); `screen` queries still find projected content. */}
<SidebarSlotProvider>
{createPortal(<SidebarSlotOutlet />, document.body)}
{children}
</SidebarSlotProvider>
</MemoryRouter>
</CoreStateContext.Provider>
</Provider>
);
}
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
}