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();
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();
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();
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(), initialized: true };
hostApiFetchMock
.mockResolvedValueOnce({ projects: [project], activeProject: null })
.mockResolvedValueOnce({ status: 'valid', config, knowledgeFiles: [] })
.mockResolvedValueOnce({
success: true,
project,
projects: [project],
activeProject: project,
});
render();
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);
});
});