import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { Login } from '@/pages/Login'; import { useAuthStore } from '@/stores/auth'; import App from '@/App'; import { useSettingsStore } from '@/stores/settings'; import { useOpencodeStore } from '@/stores/opencode'; import { useProjectConfigStore } from '@/stores/project-config'; import { createProjectConfig } from '../../shared/project-config'; const hostApiFetchMock = vi.hoisted(() => vi.fn()); vi.mock('@/lib/host-api', () => ({ hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args), })); function resetAuthStore() { useAuthStore.setState({ initialized: true, loading: false, error: null, authBase: '', clientId: 'app', accessToken: null, refreshToken: null, tokenType: null, expiresAt: null, user: null, }); } describe('Login page', () => { beforeEach(() => { window.electron.imageWorkspaceLocalDevelopment = false; window.localStorage.clear(); hostApiFetchMock.mockReset(); resetAuthStore(); useSettingsStore.getState().resetSettings(); useOpencodeStore.setState({ projects: [], activeProject: null }); useProjectConfigStore.setState({ configsByProjectId: {}, knowledgeByProjectId: {}, loadingProjectId: null, errorsByProjectId: {} }); vi.unstubAllGlobals(); }); it('keeps the login route reachable from the app router', () => { vi.stubGlobal('__NIANCODE_RENDERER_ONLY__', true); render( , ); expect(screen.getByRole('button', { name: 'Continue in browser' })).toBeInTheDocument(); }); it('redirects protected app routes to login when setup is complete but the user is signed out', async () => { useSettingsStore.setState({ setupComplete: true }); render( , ); expect(await screen.findByRole('button', { name: 'Continue in browser' })).toBeInTheDocument(); }); it('redirects an open protected route when the current auth session is invalidated', async () => { useSettingsStore.setState({ setupComplete: true }); useAuthStore.setState({ initialized: true, loading: false, error: null, authBase: 'https://biz.nianxx.cn/auth/', clientId: 'app', accessToken: 'access-token', refreshToken: 'refresh-token', tokenType: 'Bearer', expiresAt: Date.now() + 60_000, user: { username: 'brother7', userId: '1', tenantId: null, deptId: null, authorities: [], }, }); hostApiFetchMock.mockResolvedValue({ success: true }); render( , ); await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith( '/api/auth/session/sync', expect.any(Object), )); act(() => { useAuthStore.setState({ accessToken: null, refreshToken: null, tokenType: null, expiresAt: null, user: null, }); }); expect(await screen.findByRole('button', { name: 'Continue in browser' })).toBeInTheDocument(); }); it('opens only the local image workspace anonymously when the explicit development mode is active', async () => { useSettingsStore.setState({ setupComplete: true }); window.electron.imageWorkspaceLocalDevelopment = true; hostApiFetchMock.mockImplementation(async (path: string) => { if (path === '/api/works/image-workspace') { return { success: true, status: 200, data: { capabilities: { conversation: true, generation: true, image: true, video: true, }, workspaces: [], }, }; } return { success: true }; }); const { unmount } = render( , ); expect(await screen.findByText('创建第一个设计项目')).toBeInTheDocument(); expect(screen.queryByText('本地开发')).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Continue in browser' })).not.toBeInTheDocument(); unmount(); render( , ); expect(await screen.findByRole('button', { name: 'Continue in browser' })).toBeInTheDocument(); }); it('opens the project conversation from the default route for an initialized active project', async () => { vi.stubGlobal('__NIANCODE_RENDERER_ONLY__', true); const project = { id: 'project-default-chat', path: '/tmp/project-default-chat', name: 'default-chat', createdAt: '2026-07-12T00:00:00.000Z', updatedAt: '2026-07-12T00:00:00.000Z', lastOpenedAt: '2026-07-12T00:00:00.000Z', }; const config = { ...createProjectConfig(), initialized: true }; useOpencodeStore.setState({ projects: [project], activeProject: project }); hostApiFetchMock.mockImplementation(async (path: string) => { if (path === '/api/opencode/projects') return { projects: [project], activeProject: project }; if (path.startsWith('/api/opencode/projects/config?')) return { status: 'valid', config, knowledgeFiles: [] }; if (path === '/api/opencode/status') return { state: 'stopped', port: 4096 }; if (path === '/api/opencode/config-summary') return { providerIds: [], providerCount: 0, envKeys: [] }; if (path === '/api/provider-accounts') return []; if (path === '/api/provider-accounts/key-info') return []; if (path === '/api/provider-vendors') return []; if (path === '/api/provider-accounts/default') return { accountId: null }; return { success: true }; }); render( , ); expect(await screen.findByTestId('chat-operation-page')).toBeInTheDocument(); }); it('keeps the default route on project configuration without an active project', async () => { vi.stubGlobal('__NIANCODE_RENDERER_ONLY__', true); hostApiFetchMock.mockImplementation(async (path: string) => { if (path === '/api/opencode/projects') return { projects: [], activeProject: null }; if (path === '/api/opencode/status') return { state: 'stopped', port: 4096 }; if (path === '/api/opencode/config-summary') return { providerIds: [], providerCount: 0, envKeys: [] }; if (path === '/api/provider-accounts') return []; if (path === '/api/provider-accounts/key-info') return []; if (path === '/api/provider-vendors') return []; if (path === '/api/provider-accounts/default') return { accountId: null }; return { success: true }; }); render( , ); expect(await screen.findByText('一念成光,万物可创。')).toBeInTheDocument(); expect(screen.queryByText('请先新建项目')).not.toBeInTheDocument(); expect(screen.queryByTestId('chat-operation-page')).not.toBeInTheDocument(); }); it('keeps an uninitialized active project on project configuration', async () => { vi.stubGlobal('__NIANCODE_RENDERER_ONLY__', true); const project = { id: 'project-uninitialized', path: '/tmp/project-uninitialized', name: 'uninitialized', createdAt: '2026-07-12T00:00:00.000Z', updatedAt: '2026-07-12T00:00:00.000Z', lastOpenedAt: '2026-07-12T00:00:00.000Z', }; const config = { ...createProjectConfig(), initialized: false }; useOpencodeStore.setState({ projects: [project], activeProject: project }); hostApiFetchMock.mockImplementation(async (path: string) => { if (path === '/api/opencode/projects') return { projects: [project], activeProject: project }; if (path.startsWith('/api/opencode/projects/config?')) return { status: 'valid', config, knowledgeFiles: [] }; if (path === '/api/opencode/status') return { state: 'stopped', port: 4096 }; if (path === '/api/opencode/config-summary') return { providerIds: [], providerCount: 0, envKeys: [] }; if (path === '/api/provider-accounts') return []; if (path === '/api/provider-accounts/key-info') return []; if (path === '/api/provider-vendors') return []; if (path === '/api/provider-accounts/default') return { accountId: null }; return { success: true }; }); render( , ); expect(await screen.findByText('配置你的项目空间')).toBeInTheDocument(); expect(screen.queryByTestId('chat-operation-page')).not.toBeInTheDocument(); }); it('starts browser authorization and enters the app after success', async () => { hostApiFetchMock.mockImplementation(async (path: string) => { if (path === '/api/auth/browser/start') { return { success: true, token: { access_token: 'access-token', refresh_token: 'refresh-token', token_type: 'Bearer', expires_in: 43200, username: 'zhangsan', user_id: '1', }, }; } if (path === '/api/auth/session/sync') return { success: true }; if (path === '/api/provider-accounts/import-user-model-config') { return { success: true, account: { id: 'niancode-user-models', vendorId: 'custom', label: 'Makelore Models', authMode: 'api_key', model: 'gpt-4.1-mini', enabled: true, isDefault: true, createdAt: '2026-07-07T00:00:00.000Z', updatedAt: '2026-07-07T00:00:00.000Z', }, importedModels: ['gpt-4.1-mini'], }; } if (path === '/api/provider-accounts') return []; if (path === '/api/provider-accounts/key-info') return []; if (path === '/api/provider-vendors') return []; if (path === '/api/provider-accounts/default') return { accountId: null }; return { success: true }; }); render( } /> Module Selection} /> , ); expect(screen.queryByLabelText('Username')).not.toBeInTheDocument(); expect(screen.queryByLabelText('Password')).not.toBeInTheDocument(); expect(screen.queryByLabelText('Captcha')).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Continue in browser' })); await screen.findByText('Module Selection'); await waitFor(() => { expect(hostApiFetchMock).toHaveBeenCalledWith('/api/auth/browser/start', { method: 'POST', }); }); }); it('syncs the current Works Square user model config after browser login', async () => { hostApiFetchMock.mockImplementation(async (path: string) => { if (path === '/api/auth/browser/start') { return { success: true, token: { access_token: 'fresh-access-token', refresh_token: 'refresh-token', token_type: 'Bearer', expires_in: 43200, username: 'zhangsan', user_id: '1', }, }; } if (path === '/api/auth/session/sync') { return { success: true }; } if (path === '/api/provider-accounts/import-user-model-config') { return { success: true, account: { id: 'niancode-user-models', vendorId: 'custom', label: 'Makelore Models', authMode: 'api_key', model: 'gpt-4.1-mini', fallbackModels: ['claude-3-5-haiku'], enabled: true, isDefault: true, createdAt: '2026-07-07T00:00:00.000Z', updatedAt: '2026-07-07T00:00:00.000Z', }, importedModels: ['gpt-4.1-mini', 'claude-3-5-haiku'], }; } if (path === '/api/provider-accounts/default') { return { accountId: 'niancode-user-models' }; } return []; }); render( } /> Module Selection} /> , ); fireEvent.click(screen.getByRole('button', { name: 'Continue in browser' })); await waitFor(() => { expect(hostApiFetchMock).toHaveBeenCalledWith('/api/provider-accounts/import-user-model-config', { method: 'POST', body: JSON.stringify({ accessToken: 'fresh-access-token' }), }); }); }); it('keeps the user on login when model config sync fails after browser login', async () => { hostApiFetchMock .mockResolvedValueOnce({ success: true, token: { access_token: 'fresh-access-token', refresh_token: 'refresh-token', token_type: 'Bearer', expires_in: 43200, username: 'zhangsan', user_id: '1', }, }) .mockResolvedValueOnce({ success: true }) .mockRejectedValueOnce(new Error('sync failed')) .mockResolvedValueOnce({ success: true }); render( } /> Module Selection} /> , ); fireEvent.click(screen.getByRole('button', { name: 'Continue in browser' })); expect(await screen.findByText('sync failed')).toBeInTheDocument(); expect(screen.queryByText('Module Selection')).not.toBeInTheDocument(); expect(hostApiFetchMock).toHaveBeenCalledWith('/api/auth/logout', { method: 'POST', body: JSON.stringify({ accessToken: 'fresh-access-token' }), }); }); });