Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import React from 'react';
import { describe, expect, it, vi } from 'vitest';
import { DocumentsPanel } from '../../../../renderer/components/DocumentsPanel';
import type { BatchDocumentEntry } from '../../../../renderer/types';
import { LayerStackProvider } from '../../../../renderer/contexts/LayerStackContext';
import { mockTheme } from '../../../helpers/mockTheme';

function TestHost({
initialDocuments = [{ id: '1', filename: 'alpha', resetOnCompletion: false }],
}: {
initialDocuments?: BatchDocumentEntry[];
}) {
const [documents, setDocuments] = React.useState<BatchDocumentEntry[]>(initialDocuments);
const [loopEnabled, setLoopEnabled] = React.useState(false);
const [maxLoops, setMaxLoops] = React.useState<number | null>(null);

return (
<LayerStackProvider>
<DocumentsPanel
theme={mockTheme}
documents={documents}
setDocuments={setDocuments}
taskCounts={{ alpha: 2, beta: 3 }}
loadingTaskCounts={false}
loopEnabled={loopEnabled}
setLoopEnabled={setLoopEnabled}
maxLoops={maxLoops}
setMaxLoops={setMaxLoops}
allDocuments={['alpha', 'beta']}
onRefreshDocuments={vi.fn().mockResolvedValue(undefined)}
/>
</LayerStackProvider>
);
}

describe('DocumentsPanel', () => {
it('exports and renders the public component', () => {
expect(DocumentsPanel).toBeDefined();
render(<TestHost />);

expect(screen.getByText('Documents to Run')).toBeInTheDocument();
expect(screen.getByText('alpha.md')).toBeInTheDocument();
});

it('opens selector and adds newly selected documents', async () => {
render(<TestHost />);

fireEvent.click(screen.getByRole('button', { name: 'Add Docs' }));
expect(screen.getByText('Select Documents')).toBeInTheDocument();

fireEvent.click(screen.getByText('beta.md'));
fireEvent.click(screen.getByRole('button', { name: /Add 2 files/ }));

await waitFor(() => expect(screen.queryByText('Select Documents')).not.toBeInTheDocument());
expect(screen.getByText('alpha.md')).toBeInTheDocument();
expect(screen.getByText('beta.md')).toBeInTheDocument();
});

it('wires row actions and loop controls', () => {
render(
<TestHost
initialDocuments={[
{ id: '1', filename: 'alpha', resetOnCompletion: false },
{ id: '2', filename: 'beta', resetOnCompletion: false },
]}
/>
);

fireEvent.click(screen.getAllByTitle(/Enable reset/)[0]);
expect(screen.getByTitle(/Reset enabled/)).toBeInTheDocument();

fireEvent.click(screen.getByText('Loop'));
expect(screen.getByText('∞')).toBeInTheDocument();
fireEvent.click(screen.getByText('max'));
expect(screen.getByRole('slider')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { DocumentList } from '../../../../../renderer/components/DocumentsPanel/components/DocumentList';
import type { BatchDocumentEntry } from '../../../../../renderer/types';
import { mockTheme } from '../../../../helpers/mockTheme';

const documents: BatchDocumentEntry[] = [
{ id: '1', filename: 'alpha', resetOnCompletion: false },
{ id: '2', filename: 'beta', resetOnCompletion: true },
{ id: '3', filename: 'missing', resetOnCompletion: false, isMissing: true },
];

const handlers = {
handleDragStart: vi.fn(),
handleDrag: vi.fn(),
handleDragOver: vi.fn(),
handleDragLeave: vi.fn(),
handleDrop: vi.fn(),
handleDragEnd: vi.fn(),
onRemoveDocument: vi.fn(),
onToggleReset: vi.fn(),
onDuplicateDocument: vi.fn(),
};

function renderList(overrides = {}) {
const props = {
theme: mockTheme,
documents,
taskCounts: { alpha: 0, beta: 4 },
loadingTaskCounts: false,
loopEnabled: false,
draggedId: null,
dropTargetIndex: null,
isCopyDrag: false,
...handlers,
...overrides,
};
render(<DocumentList {...props} />);
return props;
}

describe('DocumentList', () => {
it('renders empty state', () => {
renderList({ documents: [] });

expect(screen.getByText('No documents selected')).toBeInTheDocument();
expect(screen.getByText(/Load a playbook/)).toBeInTheDocument();
});

it('renders rows, task badges, missing state, and actions', () => {
const props = renderList();

expect(screen.getByText('alpha.md')).toBeInTheDocument();
expect(screen.getAllByText('0 tasks').length).toBeGreaterThan(0);
expect(screen.getByText('4 tasks')).toBeInTheDocument();
expect(screen.getByText('Missing')).toBeInTheDocument();

fireEvent.click(screen.getByTitle(/Enable reset/));
expect(props.onToggleReset).toHaveBeenCalledWith('1');

fireEvent.click(screen.getByTitle('Duplicate document'));
expect(props.onDuplicateDocument).toHaveBeenCalledWith('2');

fireEvent.click(screen.getByTitle('Remove missing document'));
expect(props.onRemoveDocument).toHaveBeenCalledWith('3');
});

it('shows loop path and drop indicators', () => {
renderList({
loopEnabled: true,
draggedId: '1',
dropTargetIndex: 1,
isCopyDrag: true,
});

expect(document.querySelector('.ml-7')).toBeInTheDocument();
expect(document.querySelector('.absolute.left-0.right-0')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { DocumentSelectorModal } from '../../../../../renderer/components/DocumentsPanel/components/DocumentSelectorModal';
import type { DocTreeNode } from '../../../../../renderer/components/DocumentsPanel';
import { LayerStackProvider } from '../../../../../renderer/contexts/LayerStackContext';
import type { BatchDocumentEntry } from '../../../../../renderer/types';
import { mockTheme } from '../../../../helpers/mockTheme';

const documents: BatchDocumentEntry[] = [{ id: '1', filename: 'alpha', resetOnCompletion: false }];

const tree: DocTreeNode[] = [
{
name: 'folder',
type: 'folder',
path: 'folder',
children: [{ name: 'nested', type: 'file', path: 'folder/nested' }],
},
];

function renderModal(overrides = {}) {
const props = {
theme: mockTheme,
allDocuments: ['alpha', 'beta'],
taskCounts: { alpha: 2, beta: 0, 'folder/nested': 3 },
loadingTaskCounts: false,
documents,
onClose: vi.fn(),
onAdd: vi.fn(),
onRefresh: vi.fn().mockResolvedValue(undefined),
...overrides,
};
render(
<LayerStackProvider>
<DocumentSelectorModal {...props} />
</LayerStackProvider>
);
return props;
}

describe('DocumentSelectorModal', () => {
it('renders flat documents, counts, and add footer', () => {
const props = renderModal();
const dialog = screen.getByRole('dialog');

expect(screen.getByText('Select Documents')).toBeInTheDocument();
expect(dialog).toHaveAttribute('aria-modal', 'true');
expect(dialog).toHaveClass('select-none');
expect(screen.getAllByText('2 tasks').length).toBeGreaterThan(0);
expect(screen.getByText('alpha.md')).toBeInTheDocument();
expect(screen.getByText('beta.md')).toBeInTheDocument();

fireEvent.click(screen.getByText('beta.md'));
fireEvent.click(screen.getByRole('button', { name: /Add 2 files/ }));

expect(props.onAdd).toHaveBeenCalledWith(new Set(['alpha', 'beta']));
});

it('renders tree folders and toggles nested files', () => {
renderModal({ allDocuments: ['folder/nested'], documentTree: tree, documents: [] });

expect(screen.getByText('folder')).toBeInTheDocument();
expect(screen.queryByText('nested.md')).not.toBeInTheDocument();

const expandButton = screen.getByRole('button', { name: 'Expand folder folder' });
expect(expandButton).toHaveAttribute('aria-expanded', 'false');

fireEvent.click(expandButton);
expect(screen.getByRole('button', { name: 'Collapse folder folder' })).toHaveAttribute(
'aria-expanded',
'true'
);
expect(screen.getByText('nested.md')).toBeInTheDocument();
expect(screen.getAllByText('3 tasks').length).toBeGreaterThan(0);
});

it('shows empty state and closes from footer cancel and Escape', async () => {
const props = renderModal({ allDocuments: [], documents: [] });

expect(screen.getByText('No documents found in folder')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
expect(props.onClose).toHaveBeenCalledTimes(1);

fireEvent.keyDown(window, { key: 'Escape' });
await waitFor(() => expect(props.onClose).toHaveBeenCalledTimes(2));
});

it('refreshes and closes on backdrop without closing for inner content clicks', () => {
const props = renderModal();
const modal = screen.getByText('Select Documents').closest('.fixed')!;
const content = within(modal).getByText('alpha.md');

fireEvent.click(content);
expect(props.onClose).not.toHaveBeenCalled();

fireEvent.click(screen.getByTitle('Refresh document list'));
expect(props.onRefresh).toHaveBeenCalledTimes(1);

fireEvent.click(modal);
expect(props.onClose).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { LoopControls } from '../../../../../renderer/components/DocumentsPanel/components/LoopControls';
import type { BatchDocumentEntry } from '../../../../../renderer/types';
import { mockTheme } from '../../../../helpers/mockTheme';

const docs: BatchDocumentEntry[] = [
{ id: '1', filename: 'alpha', resetOnCompletion: false },
{ id: '2', filename: 'beta', resetOnCompletion: false },
];

function renderControls(overrides = {}) {
const props = {
theme: mockTheme,
documents: docs,
loopEnabled: false,
setLoopEnabled: vi.fn(),
maxLoops: null,
setMaxLoops: vi.fn(),
totalTaskCount: 6,
missingDocCount: 0,
hasMissingDocs: false,
loadingTaskCounts: false,
...overrides,
};
render(<LoopControls {...props} />);
return props;
}

describe('LoopControls', () => {
it('shows a one-document hint and hides loop controls', () => {
renderControls({ documents: [docs[0]] });

expect(screen.getByText('You can enable loops with two or more documents')).toBeInTheDocument();
expect(screen.queryByText('Loop')).not.toBeInTheDocument();
});

it('shows the loop hint for zero documents', () => {
renderControls({ documents: [] });

expect(screen.getByText('You can enable loops with two or more documents')).toBeInTheDocument();
expect(screen.queryByText('Loop')).not.toBeInTheDocument();
});

it('toggles loop and shows summary for available documents', () => {
const props = renderControls();

fireEvent.click(screen.getByText('Loop'));
expect(props.setLoopEnabled).toHaveBeenCalledWith(true);
expect(screen.getByText(/Total: 6 tasks across 2 documents/)).toBeInTheDocument();
});

it('switches between infinite and max loop controls', () => {
const props = renderControls({ loopEnabled: true, maxLoops: null });

fireEvent.click(screen.getByText('max'));
expect(props.setMaxLoops).toHaveBeenCalledWith(5);
expect(screen.queryByRole('slider')).not.toBeInTheDocument();
});

it('updates slider value when max mode is active', () => {
const props = renderControls({ loopEnabled: true, maxLoops: 5 });

const slider = screen.getByRole('slider');
expect(slider).toHaveAttribute('min', '1');
expect(slider).toHaveAttribute('max', '25');
fireEvent.change(slider, { target: { value: '12' } });

expect(props.setMaxLoops).toHaveBeenCalledWith(12);
fireEvent.click(screen.getByText('∞'));
expect(props.setMaxLoops).toHaveBeenCalledWith(null);
});

it('summarizes missing documents', () => {
renderControls({
documents: [
...docs,
{ id: '3', filename: 'gone', resetOnCompletion: false, isMissing: true },
],
missingDocCount: 1,
hasMissingDocs: true,
});

expect(
screen.getByText(/Total: 6 tasks across 2 available documents \(1 missing\)/)
).toBeInTheDocument();
});
});
Loading
Loading