Files
openchamber/packages/ui/src/components/session/sidebar/projects/SessionGroupSection.behavior.test.tsx
T

208 lines
7.6 KiB
TypeScript

import { describe, expect, mock, test } from 'bun:test';
import React, { act } from 'react';
import { createRoot } from 'react-dom/client';
import { I18nProvider } from '@/lib/i18n';
import { useSessionFoldersStore } from '@/stores/useSessionFoldersStore';
import { useUIStore } from '@/stores/useUIStore';
import type { SessionFolder } from '@/stores/useSessionFoldersStore';
import type { Session } from '@opencode-ai/sdk/v2';
import type { SessionGroupSectionProps } from './SessionGroupSection';
import { installHookTestDom } from '../test-utils/testDom';
type FolderCallbacks = {
onRename: (name: string) => void;
onDelete: () => void;
};
type RowPropsCapture = Pick<SessionGroupSectionProps,
| 'allowReselect'
| 'onSessionSelected'
| 'isSessionSearchOpen'
| 'sessionSearchQuery'
| 'deleteSessionConfirm'
| 'copiedSessionId'
| 'setCopiedSessionId'
>;
let folderCallbacks: FolderCallbacks | null = null;
let rowPropsCapture: RowPropsCapture | null = null;
mock.module('../../SessionFolderItem', () => ({
SessionFolderItem: (props: FolderCallbacks) => {
folderCallbacks = props;
return null;
},
}));
mock.module('../folders/sessionFolderDnd', () => ({
DroppableFolderWrapper: ({ children }: { children: (ref: () => void, isOver: boolean) => React.ReactNode }) => <>{children(() => undefined, false)}</>,
SessionFolderDndScope: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));
mock.module('@/sync/sync-context', () => ({
setActiveSession: () => undefined,
useChildStoreManager: () => ({
subscribeBootstrap: () => () => undefined,
getBootstrapState: () => null,
getBootstrapFailure: () => undefined,
requestBootstrap: () => undefined,
}),
useDirectoryStore: () => null,
useGlobalSessionStatus: () => null,
useSessionPermissions: () => null,
useSessionQuestionCount: () => 0,
useSyncSDK: () => null,
useSyncDirectory: () => null,
buildSessionMessageRecordsSnapshot: () => [],
}));
mock.module('../sessions/collapsedActivityIndicator', () => ({
CollapsedSessionActivityIndicator: () => null,
}));
mock.module('../sessions/collapsedActivityState', () => ({
useCollapsedSessionActivityState: () => null,
}));
mock.module('../sessions/SessionTreeItem', () => ({
SessionTreeItem: (props: RowPropsCapture) => {
rowPropsCapture = props;
return null;
},
}));
const { SessionGroupSection } = await import('./SessionGroupSection');
const folder: SessionFolder = {
id: 'folder-a',
name: 'Initial folder',
parentId: null,
sessionIds: [],
createdAt: 1,
};
const group: SessionGroupSectionProps['group'] = {
id: 'main',
label: 'Main',
branch: null,
description: null,
isMain: true,
worktree: null,
directory: '/workspace',
folderScopeKey: '/workspace',
sessions: [],
};
const groupWithSession: SessionGroupSectionProps['group'] = {
...group,
// SAFETY: SessionGroupSection only reads the fixture session's id in this test.
sessions: [{ session: { id: 'session-a' } as Session, children: [], worktree: null }],
};
const createProps = (): SessionGroupSectionProps => ({
group,
groupKey: 'project:main',
projectId: 'project',
hideGroupLabel: true,
hasSessionSearchQuery: false,
normalizedSessionSearchQuery: '',
groupSearchDataByGroup: new WeakMap(),
collapsedGroups: new Set(),
hideDirectoryControls: false,
showMoreGroupSessions: () => undefined,
resetGroupSessionLimit: () => undefined,
mobileVariant: false,
alwaysShowActions: false,
activeProjectId: null,
setActiveProjectIdOnly: () => undefined,
setSessionSwitcherOpen: () => undefined,
openNewSessionDraft: () => undefined,
pinnedSessionIds: new Set(),
sessionOrderIndex: new Map(),
notifyOnSubtasks: false,
expandedParents: new Set(),
editingId: null,
editTitle: '',
copiedSessionId: null,
openSidebarMenuKey: null,
setEditingId: () => undefined,
setEditTitle: () => undefined,
toggleParent: () => undefined,
setOpenSidebarMenuKey: () => undefined,
startFolderRename: () => undefined,
allowReselect: false,
isSessionSearchOpen: false,
sessionSearchQuery: '',
setSessionSearchQuery: () => undefined,
setIsSessionSearchOpen: () => undefined,
deleteSessionConfirm: null,
setDeleteSessionConfirm: () => undefined,
setCopiedSessionId: () => undefined,
onToggleCollapsedGroup: () => undefined,
folderRename: null,
setFolderRenameDraft: () => undefined,
clearFolderRename: () => undefined,
});
describe('SessionGroupSection public behavior', () => {
test('routes rendered folder rename and delete actions to the owning folder store', async () => {
const dom = installHookTestDom();
const root = createRoot(dom.container);
const originalFolders = useSessionFoldersStore.getState();
const originalUi = useUIStore.getState();
useSessionFoldersStore.setState({ foldersMap: { '/workspace': [folder] } });
useUIStore.setState({ showDeletionDialog: false });
try {
await act(async () => root.render(<I18nProvider><SessionGroupSection {...createProps()} /></I18nProvider>));
expect(folderCallbacks).not.toBeNull();
await act(async () => folderCallbacks?.onRename('Renamed folder'));
expect(useSessionFoldersStore.getState().foldersMap['/workspace']?.[0]?.name).toBe('Renamed folder');
await act(async () => folderCallbacks?.onDelete());
expect(useSessionFoldersStore.getState().foldersMap['/workspace']).toEqual([]);
} finally {
await act(async () => root.unmount());
useSessionFoldersStore.setState(originalFolders, true);
useUIStore.setState(originalUi, true);
folderCallbacks = null;
dom.restore();
}
});
test('propagates confirmation, search/navigation, and copy ownership changes to rendered rows', async () => {
const dom = installHookTestDom();
const root = createRoot(dom.container);
const firstSelected = () => undefined;
const nextSelected = () => undefined;
const firstCopied = () => undefined;
const nextCopied = () => undefined;
const initialProps = createProps();
try {
await act(async () => root.render(<I18nProvider><SessionGroupSection {...initialProps} group={groupWithSession} onSessionSelected={firstSelected} setCopiedSessionId={firstCopied} /></I18nProvider>));
expect(rowPropsCapture?.onSessionSelected).toBe(firstSelected);
expect(rowPropsCapture?.sessionSearchQuery).toBe('');
expect(rowPropsCapture?.deleteSessionConfirm).toBeNull();
expect(rowPropsCapture?.copiedSessionId).toBeNull();
expect(rowPropsCapture?.setCopiedSessionId).toBe(firstCopied);
// SAFETY: the confirmation is only forwarded by identity to the row mock.
const confirmation = { session: { id: 'session-a' } as Session, descendantCount: 0, descendantIds: [], archivedBucket: false };
await act(async () => root.render(<I18nProvider><SessionGroupSection {...initialProps} group={groupWithSession} allowReselect onSessionSelected={nextSelected} isSessionSearchOpen sessionSearchQuery="search" deleteSessionConfirm={confirmation} copiedSessionId="session-a" setCopiedSessionId={nextCopied} /></I18nProvider>));
expect(rowPropsCapture?.allowReselect).toBe(true);
expect(rowPropsCapture?.onSessionSelected).toBe(nextSelected);
expect(rowPropsCapture?.isSessionSearchOpen).toBe(true);
expect(rowPropsCapture?.sessionSearchQuery).toBe('search');
expect(rowPropsCapture?.deleteSessionConfirm).toBe(confirmation);
expect(rowPropsCapture?.copiedSessionId).toBe('session-a');
expect(rowPropsCapture?.setCopiedSessionId).toBe(nextCopied);
} finally {
await act(async () => root.unmount());
rowPropsCapture = null;
dom.restore();
}
});
});