164 lines
5.2 KiB
TypeScript
164 lines
5.2 KiB
TypeScript
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { Projects } from '@/pages/Projects';
|
|
import { useOpencodeStore } from '@/stores/opencode';
|
|
import { createProjectConfig } from '../../shared/project-config';
|
|
|
|
const hostApiFetchMock = vi.fn();
|
|
const invokeIpcMock = vi.fn();
|
|
|
|
vi.mock('@/lib/host-api', () => ({
|
|
hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args),
|
|
ensureHostApiToken: vi.fn().mockResolvedValue('test-token'),
|
|
}));
|
|
|
|
vi.mock('@/lib/api-client', () => ({
|
|
invokeIpc: (...args: unknown[]) => invokeIpcMock(...args),
|
|
}));
|
|
|
|
describe('Projects page', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
useOpencodeStore.setState({
|
|
status: { state: 'stopped', port: 4096 },
|
|
health: null,
|
|
projects: [],
|
|
activeProject: null,
|
|
sessions: [],
|
|
sessionsByProjectId: {},
|
|
selectedSessionId: null,
|
|
sessionStatuses: {},
|
|
sessionMessages: [],
|
|
sessionMessagesBySessionId: {},
|
|
streamingMessage: null,
|
|
streamingMessagesBySessionId: {},
|
|
streamingTools: [],
|
|
streamingToolsBySessionId: {},
|
|
sendingSessionId: null,
|
|
sendingSessionIds: {},
|
|
queuedSessionPrompts: {},
|
|
runtimeAutoStartAttempted: false,
|
|
loading: false,
|
|
error: null,
|
|
});
|
|
});
|
|
|
|
it('renders projects loaded from the opencode project store', async () => {
|
|
hostApiFetchMock.mockResolvedValueOnce({
|
|
projects: [{
|
|
id: 'prj_1',
|
|
path: 'D:/repo/packages/ui',
|
|
name: 'ui',
|
|
createdAt: '2026-05-12T00:00:00.000Z',
|
|
updatedAt: '2026-05-12T00:00:00.000Z',
|
|
lastOpenedAt: '2026-05-12T00:00:00.000Z',
|
|
}],
|
|
activeProject: null,
|
|
});
|
|
|
|
render(<Projects />);
|
|
|
|
await waitFor(() => expect(screen.getByText('ui')).toBeInTheDocument());
|
|
expect(screen.getByText('D:/repo/packages/ui')).toBeInTheDocument();
|
|
expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects');
|
|
});
|
|
|
|
it('opens a folder through the native picker', async () => {
|
|
const project = {
|
|
id: 'prj_2',
|
|
path: 'D:/repo/packages/api',
|
|
name: 'api',
|
|
createdAt: '2026-05-12T00:00:00.000Z',
|
|
updatedAt: '2026-05-12T00:00:00.000Z',
|
|
lastOpenedAt: '2026-05-12T00:00:00.000Z',
|
|
};
|
|
hostApiFetchMock
|
|
.mockResolvedValueOnce({ projects: [], activeProject: null })
|
|
.mockResolvedValueOnce({
|
|
success: true,
|
|
project,
|
|
projects: [project],
|
|
activeProject: project,
|
|
});
|
|
invokeIpcMock.mockResolvedValueOnce({
|
|
canceled: false,
|
|
filePaths: ['D:/repo/packages/api'],
|
|
});
|
|
|
|
render(<Projects />);
|
|
await screen.findByText('No projects yet');
|
|
fireEvent.click(screen.getAllByRole('button', { name: /open folder/i })[0]);
|
|
|
|
await waitFor(() => {
|
|
expect(invokeIpcMock).toHaveBeenCalledWith('dialog:open', {
|
|
properties: ['openDirectory'],
|
|
title: 'Open Project Folder',
|
|
});
|
|
});
|
|
expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects/open', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ path: 'D:/repo/packages/api' }),
|
|
});
|
|
});
|
|
|
|
it('does not activate a project whose configuration is missing', async () => {
|
|
const project = {
|
|
id: 'prj_missing_template',
|
|
path: 'D:/repo/packages/api',
|
|
name: 'api',
|
|
createdAt: '2026-05-12T00:00:00.000Z',
|
|
updatedAt: '2026-05-12T00:00:00.000Z',
|
|
lastOpenedAt: '2026-05-12T00:00:00.000Z',
|
|
};
|
|
hostApiFetchMock
|
|
.mockResolvedValueOnce({ projects: [project], activeProject: null })
|
|
.mockResolvedValueOnce({ status: 'missing' });
|
|
|
|
render(<Projects />);
|
|
|
|
const useButton = await screen.findByRole('button', { name: 'Use' });
|
|
fireEvent.click(useButton);
|
|
|
|
await waitFor(() => {
|
|
expect(hostApiFetchMock).toHaveBeenCalledWith(
|
|
`/api/opencode/projects/config?projectId=${encodeURIComponent(project.id)}`,
|
|
);
|
|
});
|
|
expect(useOpencodeStore.getState().activeProject).toBeNull();
|
|
expect(await screen.findByText(/configuration is missing/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it('activates a project only after its configuration validates', async () => {
|
|
const project = {
|
|
id: 'prj_valid_template',
|
|
path: 'D:/repo/packages/ui',
|
|
name: 'ui',
|
|
createdAt: '2026-05-12T00:00:00.000Z',
|
|
updatedAt: '2026-05-12T00:00:00.000Z',
|
|
lastOpenedAt: '2026-05-12T00:00:00.000Z',
|
|
};
|
|
const config = { ...createProjectConfig('standard-development'), initialized: true };
|
|
hostApiFetchMock
|
|
.mockResolvedValueOnce({ projects: [project], activeProject: null })
|
|
.mockResolvedValueOnce({ status: 'valid', config, knowledgeFiles: [] })
|
|
.mockResolvedValueOnce({
|
|
success: true,
|
|
project,
|
|
projects: [project],
|
|
activeProject: project,
|
|
});
|
|
|
|
render(<Projects />);
|
|
|
|
fireEvent.click(await screen.findByRole('button', { name: 'Use' }));
|
|
|
|
await waitFor(() => {
|
|
expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects/active', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ projectId: project.id }),
|
|
});
|
|
});
|
|
expect(useOpencodeStore.getState().activeProject).toEqual(project);
|
|
});
|
|
});
|