mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
MAESTRO: Add unit tests for session switching with file tabs
- Created comprehensive SessionContext.test.tsx with 15 unit tests - Verifies each session maintains independent file preview tabs - Tests session switching updates visible file tabs correctly - Verifies switching back restores scroll position, search query, edit mode - Tests active file tab ID is tracked per-session - Verifies rapid session switching preserves state
This commit is contained in:
609
src/__tests__/renderer/contexts/SessionContext.test.tsx
Normal file
609
src/__tests__/renderer/contexts/SessionContext.test.tsx
Normal file
@@ -0,0 +1,609 @@
|
||||
/**
|
||||
* Tests for SessionContext - Session Switching with File Tabs
|
||||
*
|
||||
* This test suite verifies that:
|
||||
* 1. Each session maintains its own file tabs independently
|
||||
* 2. Session switching properly switches to new session's file tabs
|
||||
* 3. Switching back to a session restores its file tabs correctly
|
||||
* 4. File tab state (scroll position, search query, edit mode) is per-session
|
||||
* 5. Active file tab ID is per-session
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { render, screen, renderHook, act } from '@testing-library/react';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { SessionProvider, useSession } from '../../../renderer/contexts/SessionContext';
|
||||
import type { Session, AITab, FilePreviewTab, UnifiedTabRef } from '../../../renderer/types';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Create a minimal AITab with sensible defaults */
|
||||
const makeTab = (overrides: Partial<AITab> = {}): AITab => ({
|
||||
id: overrides.id ?? `tab-${Math.random().toString(36).slice(2, 8)}`,
|
||||
agentSessionId: null,
|
||||
name: null,
|
||||
starred: false,
|
||||
logs: [],
|
||||
inputValue: '',
|
||||
stagedImages: [],
|
||||
createdAt: Date.now(),
|
||||
state: 'idle',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
/** Create a minimal FilePreviewTab for testing */
|
||||
const makeFilePreviewTab = (overrides: Partial<FilePreviewTab> = {}): FilePreviewTab => ({
|
||||
id: overrides.id ?? `file-tab-${Math.random().toString(36).slice(2, 8)}`,
|
||||
path: overrides.path ?? '/test/file.ts',
|
||||
name: overrides.name ?? 'file',
|
||||
extension: overrides.extension ?? '.ts',
|
||||
content: overrides.content ?? 'console.log("test");',
|
||||
scrollTop: overrides.scrollTop ?? 0,
|
||||
searchQuery: overrides.searchQuery ?? '',
|
||||
editMode: overrides.editMode ?? false,
|
||||
editContent: overrides.editContent ?? undefined,
|
||||
createdAt: overrides.createdAt ?? Date.now(),
|
||||
lastModified: overrides.lastModified ?? Date.now(),
|
||||
sshRemoteId: overrides.sshRemoteId,
|
||||
isLoading: overrides.isLoading,
|
||||
});
|
||||
|
||||
/** Create a minimal Session with sensible defaults */
|
||||
const makeSession = (overrides: Partial<Session> = {}): Session => {
|
||||
const defaultTab = makeTab({ id: 'default-tab' });
|
||||
return {
|
||||
id: overrides.id ?? `session-${Math.random().toString(36).slice(2, 8)}`,
|
||||
name: 'Test Session',
|
||||
toolType: 'claude-code',
|
||||
state: 'idle',
|
||||
cwd: '/test',
|
||||
fullPath: '/test',
|
||||
projectRoot: '/test',
|
||||
aiLogs: [],
|
||||
shellLogs: [],
|
||||
workLog: [],
|
||||
contextUsage: 0,
|
||||
inputMode: 'ai',
|
||||
aiPid: 0,
|
||||
terminalPid: 0,
|
||||
port: 0,
|
||||
isLive: false,
|
||||
changedFiles: [],
|
||||
isGitRepo: false,
|
||||
fileTree: [],
|
||||
fileExplorerExpanded: [],
|
||||
fileExplorerScrollPos: 0,
|
||||
executionQueue: [],
|
||||
activeTimeMs: 0,
|
||||
aiTabs: [defaultTab],
|
||||
activeTabId: defaultTab.id,
|
||||
closedTabHistory: [],
|
||||
filePreviewTabs: overrides.filePreviewTabs ?? [],
|
||||
activeFileTabId: overrides.activeFileTabId ?? null,
|
||||
unifiedTabOrder: overrides.unifiedTabOrder ?? [{ type: 'ai' as const, id: defaultTab.id }],
|
||||
unifiedClosedTabHistory: overrides.unifiedClosedTabHistory ?? [],
|
||||
...overrides,
|
||||
} as Session;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test Wrapper Component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A test component that exposes session context for testing.
|
||||
* This component simulates an app that manages multiple sessions.
|
||||
*/
|
||||
interface TestAppProps {
|
||||
initialSessions: Session[];
|
||||
initialActiveSessionId: string;
|
||||
onSessionChange?: (sessionId: string) => void;
|
||||
}
|
||||
|
||||
function TestApp({ initialSessions, initialActiveSessionId, onSessionChange }: TestAppProps) {
|
||||
const { sessions, setSessions, activeSessionId, setActiveSessionId, activeSession } = useSession();
|
||||
|
||||
// Initialize sessions on first render
|
||||
React.useEffect(() => {
|
||||
if (sessions.length === 0 && initialSessions.length > 0) {
|
||||
setSessions(initialSessions);
|
||||
setActiveSessionId(initialActiveSessionId);
|
||||
}
|
||||
}, [initialSessions, initialActiveSessionId, sessions.length, setSessions, setActiveSessionId]);
|
||||
|
||||
const handleSwitch = useCallback((id: string) => {
|
||||
setActiveSessionId(id);
|
||||
onSessionChange?.(id);
|
||||
}, [setActiveSessionId, onSessionChange]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="active-session-id">{activeSessionId}</div>
|
||||
<div data-testid="active-session-name">{activeSession?.name ?? 'none'}</div>
|
||||
<div data-testid="file-tabs-count">{activeSession?.filePreviewTabs?.length ?? 0}</div>
|
||||
<div data-testid="active-file-tab-id">{activeSession?.activeFileTabId ?? 'none'}</div>
|
||||
{sessions.map(session => (
|
||||
<button
|
||||
key={session.id}
|
||||
data-testid={`switch-to-${session.id}`}
|
||||
onClick={() => handleSwitch(session.id)}
|
||||
>
|
||||
{session.name}
|
||||
</button>
|
||||
))}
|
||||
{activeSession?.filePreviewTabs?.map(tab => (
|
||||
<div key={tab.id} data-testid={`file-tab-${tab.id}`}>
|
||||
<span data-testid={`file-path-${tab.id}`}>{tab.path}</span>
|
||||
<span data-testid={`file-scroll-${tab.id}`}>{tab.scrollTop}</span>
|
||||
<span data-testid={`file-search-${tab.id}`}>{tab.searchQuery}</span>
|
||||
<span data-testid={`file-edit-${tab.id}`}>{tab.editMode ? 'editing' : 'viewing'}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TestAppWrapper(props: TestAppProps) {
|
||||
return (
|
||||
<SessionProvider>
|
||||
<TestApp {...props} />
|
||||
</SessionProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test Suite
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('SessionContext - Session Switching with File Tabs', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('independent file tabs per session', () => {
|
||||
it('each session maintains its own file tabs', () => {
|
||||
const session1FileTab = makeFilePreviewTab({
|
||||
id: 's1-file',
|
||||
path: '/session1/app.ts',
|
||||
name: 'app',
|
||||
scrollTop: 100,
|
||||
});
|
||||
const session2FileTab = makeFilePreviewTab({
|
||||
id: 's2-file',
|
||||
path: '/session2/index.ts',
|
||||
name: 'index',
|
||||
scrollTop: 500,
|
||||
});
|
||||
|
||||
const session1 = makeSession({
|
||||
id: 'session-1',
|
||||
name: 'Session 1',
|
||||
filePreviewTabs: [session1FileTab],
|
||||
activeFileTabId: 's1-file',
|
||||
});
|
||||
const session2 = makeSession({
|
||||
id: 'session-2',
|
||||
name: 'Session 2',
|
||||
filePreviewTabs: [session2FileTab],
|
||||
activeFileTabId: 's2-file',
|
||||
});
|
||||
|
||||
render(<TestAppWrapper initialSessions={[session1, session2]} initialActiveSessionId="session-1" />);
|
||||
|
||||
// Session 1 is active - should see its file tabs
|
||||
expect(screen.getByTestId('active-session-id')).toHaveTextContent('session-1');
|
||||
expect(screen.getByTestId('file-tabs-count')).toHaveTextContent('1');
|
||||
expect(screen.getByTestId('file-path-s1-file')).toHaveTextContent('/session1/app.ts');
|
||||
expect(screen.getByTestId('file-scroll-s1-file')).toHaveTextContent('100');
|
||||
});
|
||||
|
||||
it('session with no file tabs shows count of 0', () => {
|
||||
const session = makeSession({
|
||||
id: 'session-no-files',
|
||||
name: 'No Files Session',
|
||||
filePreviewTabs: [],
|
||||
activeFileTabId: null,
|
||||
});
|
||||
|
||||
render(<TestAppWrapper initialSessions={[session]} initialActiveSessionId="session-no-files" />);
|
||||
|
||||
expect(screen.getByTestId('file-tabs-count')).toHaveTextContent('0');
|
||||
expect(screen.getByTestId('active-file-tab-id')).toHaveTextContent('none');
|
||||
});
|
||||
|
||||
it('session with multiple file tabs shows all tabs', () => {
|
||||
const tabs = [
|
||||
makeFilePreviewTab({ id: 'f1', path: '/src/a.ts' }),
|
||||
makeFilePreviewTab({ id: 'f2', path: '/src/b.ts' }),
|
||||
makeFilePreviewTab({ id: 'f3', path: '/src/c.ts' }),
|
||||
];
|
||||
|
||||
const session = makeSession({
|
||||
id: 'multi-tab-session',
|
||||
name: 'Multi Tab Session',
|
||||
filePreviewTabs: tabs,
|
||||
activeFileTabId: 'f2',
|
||||
});
|
||||
|
||||
render(<TestAppWrapper initialSessions={[session]} initialActiveSessionId="multi-tab-session" />);
|
||||
|
||||
expect(screen.getByTestId('file-tabs-count')).toHaveTextContent('3');
|
||||
expect(screen.getByTestId('file-path-f1')).toHaveTextContent('/src/a.ts');
|
||||
expect(screen.getByTestId('file-path-f2')).toHaveTextContent('/src/b.ts');
|
||||
expect(screen.getByTestId('file-path-f3')).toHaveTextContent('/src/c.ts');
|
||||
expect(screen.getByTestId('active-file-tab-id')).toHaveTextContent('f2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('session switching updates file tabs', () => {
|
||||
it('switches to the new session file tabs when session is changed', async () => {
|
||||
const session1 = makeSession({
|
||||
id: 'session-1',
|
||||
name: 'Session 1',
|
||||
filePreviewTabs: [makeFilePreviewTab({ id: 's1-file', path: '/s1/file.ts' })],
|
||||
activeFileTabId: 's1-file',
|
||||
});
|
||||
const session2 = makeSession({
|
||||
id: 'session-2',
|
||||
name: 'Session 2',
|
||||
filePreviewTabs: [makeFilePreviewTab({ id: 's2-file', path: '/s2/file.ts' })],
|
||||
activeFileTabId: 's2-file',
|
||||
});
|
||||
|
||||
render(<TestAppWrapper initialSessions={[session1, session2]} initialActiveSessionId="session-1" />);
|
||||
|
||||
// Verify initial state - session 1 active
|
||||
expect(screen.getByTestId('active-session-id')).toHaveTextContent('session-1');
|
||||
expect(screen.getByTestId('file-path-s1-file')).toHaveTextContent('/s1/file.ts');
|
||||
|
||||
// Switch to session 2
|
||||
await act(async () => {
|
||||
screen.getByTestId('switch-to-session-2').click();
|
||||
});
|
||||
|
||||
// Verify session 2's file tabs are now showing
|
||||
expect(screen.getByTestId('active-session-id')).toHaveTextContent('session-2');
|
||||
expect(screen.queryByTestId('file-path-s1-file')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('file-path-s2-file')).toHaveTextContent('/s2/file.ts');
|
||||
});
|
||||
|
||||
it('switching from session with files to session without files shows empty tabs', async () => {
|
||||
const session1 = makeSession({
|
||||
id: 'session-with-files',
|
||||
name: 'With Files',
|
||||
filePreviewTabs: [makeFilePreviewTab({ id: 'f1', path: '/file.ts' })],
|
||||
activeFileTabId: 'f1',
|
||||
});
|
||||
const session2 = makeSession({
|
||||
id: 'session-no-files',
|
||||
name: 'No Files',
|
||||
filePreviewTabs: [],
|
||||
activeFileTabId: null,
|
||||
});
|
||||
|
||||
render(<TestAppWrapper initialSessions={[session1, session2]} initialActiveSessionId="session-with-files" />);
|
||||
|
||||
// Session 1 has files
|
||||
expect(screen.getByTestId('file-tabs-count')).toHaveTextContent('1');
|
||||
|
||||
// Switch to session 2
|
||||
await act(async () => {
|
||||
screen.getByTestId('switch-to-session-no-files').click();
|
||||
});
|
||||
|
||||
// Session 2 has no files
|
||||
expect(screen.getByTestId('file-tabs-count')).toHaveTextContent('0');
|
||||
expect(screen.getByTestId('active-file-tab-id')).toHaveTextContent('none');
|
||||
});
|
||||
|
||||
it('switching from session without files to session with files shows files', async () => {
|
||||
const session1 = makeSession({
|
||||
id: 'session-no-files',
|
||||
name: 'No Files',
|
||||
filePreviewTabs: [],
|
||||
activeFileTabId: null,
|
||||
});
|
||||
const session2 = makeSession({
|
||||
id: 'session-with-files',
|
||||
name: 'With Files',
|
||||
filePreviewTabs: [
|
||||
makeFilePreviewTab({ id: 'f1', path: '/a.ts' }),
|
||||
makeFilePreviewTab({ id: 'f2', path: '/b.ts' }),
|
||||
],
|
||||
activeFileTabId: 'f1',
|
||||
});
|
||||
|
||||
render(<TestAppWrapper initialSessions={[session1, session2]} initialActiveSessionId="session-no-files" />);
|
||||
|
||||
// Start with no files
|
||||
expect(screen.getByTestId('file-tabs-count')).toHaveTextContent('0');
|
||||
|
||||
// Switch to session with files
|
||||
await act(async () => {
|
||||
screen.getByTestId('switch-to-session-with-files').click();
|
||||
});
|
||||
|
||||
// Now should see files
|
||||
expect(screen.getByTestId('file-tabs-count')).toHaveTextContent('2');
|
||||
expect(screen.getByTestId('file-path-f1')).toHaveTextContent('/a.ts');
|
||||
expect(screen.getByTestId('file-path-f2')).toHaveTextContent('/b.ts');
|
||||
});
|
||||
});
|
||||
|
||||
describe('switching back restores file tabs', () => {
|
||||
it('switching back to a session restores its file tabs correctly', async () => {
|
||||
const session1 = makeSession({
|
||||
id: 'session-1',
|
||||
name: 'Session 1',
|
||||
filePreviewTabs: [makeFilePreviewTab({ id: 's1-file', path: '/s1/original.ts' })],
|
||||
activeFileTabId: 's1-file',
|
||||
});
|
||||
const session2 = makeSession({
|
||||
id: 'session-2',
|
||||
name: 'Session 2',
|
||||
filePreviewTabs: [makeFilePreviewTab({ id: 's2-file', path: '/s2/original.ts' })],
|
||||
activeFileTabId: 's2-file',
|
||||
});
|
||||
|
||||
render(<TestAppWrapper initialSessions={[session1, session2]} initialActiveSessionId="session-1" />);
|
||||
|
||||
// Start at session 1
|
||||
expect(screen.getByTestId('file-path-s1-file')).toHaveTextContent('/s1/original.ts');
|
||||
|
||||
// Switch to session 2
|
||||
await act(async () => {
|
||||
screen.getByTestId('switch-to-session-2').click();
|
||||
});
|
||||
expect(screen.getByTestId('file-path-s2-file')).toHaveTextContent('/s2/original.ts');
|
||||
|
||||
// Switch back to session 1
|
||||
await act(async () => {
|
||||
screen.getByTestId('switch-to-session-1').click();
|
||||
});
|
||||
|
||||
// Session 1's file tabs are restored
|
||||
expect(screen.getByTestId('file-path-s1-file')).toHaveTextContent('/s1/original.ts');
|
||||
expect(screen.queryByTestId('file-path-s2-file')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('preserves scroll position per session when switching', async () => {
|
||||
const session1 = makeSession({
|
||||
id: 'session-1',
|
||||
name: 'Session 1',
|
||||
filePreviewTabs: [makeFilePreviewTab({ id: 's1-file', scrollTop: 1500 })],
|
||||
activeFileTabId: 's1-file',
|
||||
});
|
||||
const session2 = makeSession({
|
||||
id: 'session-2',
|
||||
name: 'Session 2',
|
||||
filePreviewTabs: [makeFilePreviewTab({ id: 's2-file', scrollTop: 3000 })],
|
||||
activeFileTabId: 's2-file',
|
||||
});
|
||||
|
||||
render(<TestAppWrapper initialSessions={[session1, session2]} initialActiveSessionId="session-1" />);
|
||||
|
||||
// Session 1 scroll position
|
||||
expect(screen.getByTestId('file-scroll-s1-file')).toHaveTextContent('1500');
|
||||
|
||||
// Switch to session 2
|
||||
await act(async () => {
|
||||
screen.getByTestId('switch-to-session-2').click();
|
||||
});
|
||||
expect(screen.getByTestId('file-scroll-s2-file')).toHaveTextContent('3000');
|
||||
|
||||
// Switch back to session 1
|
||||
await act(async () => {
|
||||
screen.getByTestId('switch-to-session-1').click();
|
||||
});
|
||||
|
||||
// Session 1's scroll position is preserved
|
||||
expect(screen.getByTestId('file-scroll-s1-file')).toHaveTextContent('1500');
|
||||
});
|
||||
|
||||
it('preserves search query per session when switching', async () => {
|
||||
const session1 = makeSession({
|
||||
id: 'session-1',
|
||||
name: 'Session 1',
|
||||
filePreviewTabs: [makeFilePreviewTab({ id: 's1-file', searchQuery: 'handleClick' })],
|
||||
activeFileTabId: 's1-file',
|
||||
});
|
||||
const session2 = makeSession({
|
||||
id: 'session-2',
|
||||
name: 'Session 2',
|
||||
filePreviewTabs: [makeFilePreviewTab({ id: 's2-file', searchQuery: 'useState' })],
|
||||
activeFileTabId: 's2-file',
|
||||
});
|
||||
|
||||
render(<TestAppWrapper initialSessions={[session1, session2]} initialActiveSessionId="session-1" />);
|
||||
|
||||
// Session 1 search query
|
||||
expect(screen.getByTestId('file-search-s1-file')).toHaveTextContent('handleClick');
|
||||
|
||||
// Switch to session 2
|
||||
await act(async () => {
|
||||
screen.getByTestId('switch-to-session-2').click();
|
||||
});
|
||||
expect(screen.getByTestId('file-search-s2-file')).toHaveTextContent('useState');
|
||||
|
||||
// Switch back to session 1
|
||||
await act(async () => {
|
||||
screen.getByTestId('switch-to-session-1').click();
|
||||
});
|
||||
|
||||
// Session 1's search query is preserved
|
||||
expect(screen.getByTestId('file-search-s1-file')).toHaveTextContent('handleClick');
|
||||
});
|
||||
|
||||
it('preserves edit mode per session when switching', async () => {
|
||||
const session1 = makeSession({
|
||||
id: 'session-1',
|
||||
name: 'Session 1',
|
||||
filePreviewTabs: [makeFilePreviewTab({ id: 's1-file', editMode: true })],
|
||||
activeFileTabId: 's1-file',
|
||||
});
|
||||
const session2 = makeSession({
|
||||
id: 'session-2',
|
||||
name: 'Session 2',
|
||||
filePreviewTabs: [makeFilePreviewTab({ id: 's2-file', editMode: false })],
|
||||
activeFileTabId: 's2-file',
|
||||
});
|
||||
|
||||
render(<TestAppWrapper initialSessions={[session1, session2]} initialActiveSessionId="session-1" />);
|
||||
|
||||
// Session 1 edit mode
|
||||
expect(screen.getByTestId('file-edit-s1-file')).toHaveTextContent('editing');
|
||||
|
||||
// Switch to session 2
|
||||
await act(async () => {
|
||||
screen.getByTestId('switch-to-session-2').click();
|
||||
});
|
||||
expect(screen.getByTestId('file-edit-s2-file')).toHaveTextContent('viewing');
|
||||
|
||||
// Switch back to session 1
|
||||
await act(async () => {
|
||||
screen.getByTestId('switch-to-session-1').click();
|
||||
});
|
||||
|
||||
// Session 1's edit mode is preserved
|
||||
expect(screen.getByTestId('file-edit-s1-file')).toHaveTextContent('editing');
|
||||
});
|
||||
});
|
||||
|
||||
describe('active file tab ID per session', () => {
|
||||
it('each session tracks its own active file tab', async () => {
|
||||
const session1 = makeSession({
|
||||
id: 'session-1',
|
||||
name: 'Session 1',
|
||||
filePreviewTabs: [
|
||||
makeFilePreviewTab({ id: 's1-f1' }),
|
||||
makeFilePreviewTab({ id: 's1-f2' }),
|
||||
],
|
||||
activeFileTabId: 's1-f2', // Second tab is active
|
||||
});
|
||||
const session2 = makeSession({
|
||||
id: 'session-2',
|
||||
name: 'Session 2',
|
||||
filePreviewTabs: [
|
||||
makeFilePreviewTab({ id: 's2-f1' }),
|
||||
makeFilePreviewTab({ id: 's2-f2' }),
|
||||
makeFilePreviewTab({ id: 's2-f3' }),
|
||||
],
|
||||
activeFileTabId: 's2-f1', // First tab is active
|
||||
});
|
||||
|
||||
render(<TestAppWrapper initialSessions={[session1, session2]} initialActiveSessionId="session-1" />);
|
||||
|
||||
// Session 1 has second tab active
|
||||
expect(screen.getByTestId('active-file-tab-id')).toHaveTextContent('s1-f2');
|
||||
|
||||
// Switch to session 2
|
||||
await act(async () => {
|
||||
screen.getByTestId('switch-to-session-2').click();
|
||||
});
|
||||
|
||||
// Session 2 has first tab active
|
||||
expect(screen.getByTestId('active-file-tab-id')).toHaveTextContent('s2-f1');
|
||||
|
||||
// Switch back to session 1
|
||||
await act(async () => {
|
||||
screen.getByTestId('switch-to-session-1').click();
|
||||
});
|
||||
|
||||
// Session 1 still has second tab active
|
||||
expect(screen.getByTestId('active-file-tab-id')).toHaveTextContent('s1-f2');
|
||||
});
|
||||
|
||||
it('session with AI tab active has null activeFileTabId', async () => {
|
||||
const session1 = makeSession({
|
||||
id: 'session-1',
|
||||
name: 'Session 1 - AI active',
|
||||
filePreviewTabs: [makeFilePreviewTab({ id: 'f1' })],
|
||||
activeFileTabId: null, // AI tab is active, not file tab
|
||||
});
|
||||
const session2 = makeSession({
|
||||
id: 'session-2',
|
||||
name: 'Session 2 - File active',
|
||||
filePreviewTabs: [makeFilePreviewTab({ id: 'f2' })],
|
||||
activeFileTabId: 'f2',
|
||||
});
|
||||
|
||||
render(<TestAppWrapper initialSessions={[session1, session2]} initialActiveSessionId="session-1" />);
|
||||
|
||||
// Session 1 has no active file tab (AI is active)
|
||||
expect(screen.getByTestId('active-file-tab-id')).toHaveTextContent('none');
|
||||
|
||||
// Switch to session 2
|
||||
await act(async () => {
|
||||
screen.getByTestId('switch-to-session-2').click();
|
||||
});
|
||||
|
||||
// Session 2 has file tab active
|
||||
expect(screen.getByTestId('active-file-tab-id')).toHaveTextContent('f2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('rapid session switching', () => {
|
||||
it('handles rapid session switching without losing state', async () => {
|
||||
const sessions = Array.from({ length: 5 }, (_, i) =>
|
||||
makeSession({
|
||||
id: `session-${i}`,
|
||||
name: `Session ${i}`,
|
||||
filePreviewTabs: [makeFilePreviewTab({ id: `f${i}`, path: `/path/${i}.ts`, scrollTop: i * 100 })],
|
||||
activeFileTabId: `f${i}`,
|
||||
})
|
||||
);
|
||||
|
||||
render(<TestAppWrapper initialSessions={sessions} initialActiveSessionId="session-0" />);
|
||||
|
||||
// Rapid switching through all sessions
|
||||
for (let i = 1; i < 5; i++) {
|
||||
await act(async () => {
|
||||
screen.getByTestId(`switch-to-session-${i}`).click();
|
||||
});
|
||||
expect(screen.getByTestId('active-session-id')).toHaveTextContent(`session-${i}`);
|
||||
expect(screen.getByTestId(`file-scroll-f${i}`)).toHaveTextContent(String(i * 100));
|
||||
}
|
||||
|
||||
// Switch back to first session
|
||||
await act(async () => {
|
||||
screen.getByTestId('switch-to-session-0').click();
|
||||
});
|
||||
|
||||
// Original state preserved
|
||||
expect(screen.getByTestId('active-session-id')).toHaveTextContent('session-0');
|
||||
expect(screen.getByTestId('file-scroll-f0')).toHaveTextContent('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hook usage', () => {
|
||||
it('throws error when used outside SessionProvider', () => {
|
||||
// Suppress console.error for this test
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
expect(() => {
|
||||
renderHook(() => useSession());
|
||||
}).toThrow('useSession must be used within a SessionProvider');
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('provides stable setActiveSessionId callback', () => {
|
||||
const { result, rerender } = renderHook(() => useSession(), {
|
||||
wrapper: SessionProvider,
|
||||
});
|
||||
|
||||
const firstCallback = result.current.setActiveSessionId;
|
||||
|
||||
rerender();
|
||||
|
||||
// setActiveSessionId should be the same reference after rerender
|
||||
expect(result.current.setActiveSessionId).toBe(firstCallback);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user