162 lines
4.9 KiB
TypeScript
162 lines
4.9 KiB
TypeScript
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||
import { MemoryRouter } from 'react-router-dom';
|
||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||
import Setup from '@/pages/Setup';
|
||
import { useAuthStore } from '@/stores/auth';
|
||
import { useOpencodeStore } from '@/stores/opencode';
|
||
import { useSettingsStore } from '@/stores/settings';
|
||
|
||
const { hostApiFetchMock, translateMock } = vi.hoisted(() => ({
|
||
hostApiFetchMock: vi.fn(),
|
||
translateMock: vi.fn((key: string, options?: { returnObjects?: boolean }) => (
|
||
options?.returnObjects ? [`${key}.0`, `${key}.1`, `${key}.2`] : key
|
||
)),
|
||
}));
|
||
|
||
vi.mock('@/lib/host-api', () => ({
|
||
hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args),
|
||
}));
|
||
|
||
vi.mock('@/lib/api-client', () => ({
|
||
invokeIpc: vi.fn(),
|
||
}));
|
||
|
||
vi.mock('react-i18next', async (importOriginal) => {
|
||
const actual = await importOriginal<typeof import('react-i18next')>();
|
||
return {
|
||
...actual,
|
||
useTranslation: () => ({
|
||
t: translateMock,
|
||
}),
|
||
};
|
||
});
|
||
|
||
vi.mock('sonner', () => ({
|
||
toast: {
|
||
success: vi.fn(),
|
||
error: vi.fn(),
|
||
},
|
||
}));
|
||
|
||
function renderSetup() {
|
||
return render(
|
||
<MemoryRouter initialEntries={['/setup']}>
|
||
<Setup />
|
||
</MemoryRouter>,
|
||
);
|
||
}
|
||
|
||
function resetStores() {
|
||
useAuthStore.setState({ user: null });
|
||
useSettingsStore.setState({
|
||
setupComplete: false,
|
||
theme: 'system',
|
||
language: 'en',
|
||
launchAtStartup: false,
|
||
telemetryEnabled: false,
|
||
proxyEnabled: false,
|
||
proxyServer: '',
|
||
proxyHttpServer: '',
|
||
proxyHttpsServer: '',
|
||
proxyAllServer: '',
|
||
proxyBypassRules: '',
|
||
sidebarCollapsed: false,
|
||
devModeUnlocked: false,
|
||
});
|
||
useOpencodeStore.setState({
|
||
status: { state: 'stopped', port: 4096 },
|
||
health: null,
|
||
healthCheckedAt: null,
|
||
runtimeConfigSummary: null,
|
||
projects: [],
|
||
activeProject: null,
|
||
sessions: [],
|
||
sessionsByProjectId: {},
|
||
selectedSessionId: null,
|
||
sessionStatuses: {},
|
||
sessionMessages: [],
|
||
sessionMessagesBySessionId: {},
|
||
streamingMessage: null,
|
||
streamingMessagesBySessionId: {},
|
||
streamingTools: [],
|
||
streamingToolsBySessionId: {},
|
||
sendingSessionId: null,
|
||
sendingSessionIds: {},
|
||
queuedSessionPrompts: {},
|
||
sessionDiffs: [],
|
||
sessionDiffLoading: false,
|
||
sessionDiffError: null,
|
||
fileStatuses: [],
|
||
fileSearchResults: [],
|
||
contentSearchResults: [],
|
||
selectedContextFile: null,
|
||
fileContextLoading: false,
|
||
fileContextError: null,
|
||
runtimeAutoStartAttempted: false,
|
||
loading: false,
|
||
error: null,
|
||
});
|
||
}
|
||
|
||
describe('Setup first-run progress flow', () => {
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
resetStores();
|
||
});
|
||
|
||
it('greets the authenticated user by the normalized account name', () => {
|
||
useAuthStore.setState({
|
||
user: { username: 'zhangsan@example.com', userId: 'user-1', tenantId: null, deptId: null, authorities: [] },
|
||
});
|
||
|
||
renderSetup();
|
||
|
||
expect(screen.getByRole('heading', { name: 'zhangsan,欢迎回来!' })).toBeInTheDocument();
|
||
});
|
||
|
||
it('renders runtime checks as a quiet progress flow without dependency details', async () => {
|
||
hostApiFetchMock.mockResolvedValue({ state: 'stopped', port: 4096 });
|
||
|
||
renderSetup();
|
||
|
||
fireEvent.click(screen.getByTestId('setup-next-button'));
|
||
|
||
expect(await screen.findByTestId('setup-runtime-progress')).toBeInTheDocument();
|
||
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||
expect(screen.queryByText('runtime.nodejs')).not.toBeInTheDocument();
|
||
expect(screen.queryByText('runtime.opencode')).not.toBeInTheDocument();
|
||
expect(screen.queryByRole('button', { name: 'runtime.viewLogs' })).not.toBeInTheDocument();
|
||
});
|
||
|
||
it('shows only a retry action when runtime checks fail', async () => {
|
||
hostApiFetchMock.mockResolvedValue({
|
||
state: 'error',
|
||
error: 'missing bundled runtime package at /very/noisy/path',
|
||
});
|
||
|
||
renderSetup();
|
||
|
||
fireEvent.click(screen.getByTestId('setup-next-button'));
|
||
|
||
expect(await screen.findByTestId('setup-runtime-retry')).toBeInTheDocument();
|
||
expect(screen.queryByText(/missing bundled runtime package/)).not.toBeInTheDocument();
|
||
expect(screen.queryByRole('button', { name: 'runtime.viewLogs' })).not.toBeInTheDocument();
|
||
});
|
||
|
||
it('keeps dependency installation focused on overall progress', async () => {
|
||
hostApiFetchMock.mockResolvedValue({ state: 'stopped', port: 4096 });
|
||
|
||
renderSetup();
|
||
|
||
fireEvent.click(screen.getByTestId('setup-next-button'));
|
||
|
||
await waitFor(() => expect(screen.getByTestId('setup-next-button')).toBeEnabled());
|
||
fireEvent.click(screen.getByTestId('setup-next-button'));
|
||
|
||
expect(await screen.findByTestId('setup-installing-progress')).toBeInTheDocument();
|
||
expect(screen.queryByText('defaultSkills.python-env.name')).not.toBeInTheDocument();
|
||
expect(screen.queryByText('defaultSkills.file-tools.name')).not.toBeInTheDocument();
|
||
expect(screen.queryByRole('button', { name: 'installing.skip' })).not.toBeInTheDocument();
|
||
});
|
||
});
|