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(); return { ...actual, useTranslation: () => ({ t: translateMock, }), }; }); vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn(), }, })); function renderSetup() { return render( , ); } 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(); }); });