208 lines
7.6 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
});
|