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, tokenType: null, expiresAt: null, lastActiveAt: null, canRefresh: false, legacyRefreshToken: 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', async () => { vi.stubGlobal('__NIANCODE_RENDERER_ONLY__', true); render( , ); expect(await screen.findByRole('button', { name: '在浏览器中继续' })).toBeInTheDocument(); }); it('uses Chinese copy and keeps upstream HTML errors concise', () => { useAuthStore.setState({ error: '502 Bad Gatewayupstream details', }); render( , ); expect(screen.getByText('登录 Makelore 账户')).toBeInTheDocument(); expect(screen.getByText('请在浏览器中完成登录,并授权此桌面应用访问。')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '在浏览器中继续' })).toBeInTheDocument(); expect(screen.getByRole('alert')).toHaveTextContent('登录服务暂时不可用,请稍后重试。'); expect(screen.queryByText(//i)).not.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: '在浏览器中继续' })).toBeInTheDocument(); }); it('opens the module chooser at the default root route', async () => { vi.stubGlobal('__NIANCODE_RENDERER_ONLY__', true); render( , ); expect(await screen.findByTestId('ai-module-selection-page')).toBeInTheDocument(); expect(screen.getByRole('heading', { name: '今天,想创作什么?' })).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', tokenType: 'Bearer', expiresAt: Date.now() + 60_000, lastActiveAt: Date.now(), canRefresh: true, legacyRefreshToken: null, 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, tokenType: null, expiresAt: null, lastActiveAt: null, canRefresh: false, legacyRefreshToken: null, user: null, }); }); expect(await screen.findByRole('button', { name: '在浏览器中继续' })).toBeInTheDocument(); }); it('does not record synthetic DOM events as user activity', async () => { useSettingsStore.setState({ setupComplete: true }); const lastActiveAt = Date.now() - 24 * 60 * 60 * 1000; useAuthStore.setState({ initialized: true, loading: false, error: null, authBase: 'https://biz.nianxx.cn/auth/', clientId: 'app', accessToken: 'access-token', tokenType: 'Bearer', expiresAt: Date.now() + 3600_000, lastActiveAt, canRefresh: true, legacyRefreshToken: null, user: { username: 'brother7', userId: '1', tenantId: null, deptId: null, authorities: [], }, }); hostApiFetchMock.mockImplementation(async (path: string) => { if (path === '/api/auth/session/activity') { return { success: true, session: { accessToken: 'access-token', tokenType: 'Bearer', expiresAt: Date.now() + 3600_000, lastActiveAt: Date.now(), canRefresh: true, }, }; } if (path === '/api/auth/session/sync') { return { success: true, session: { accessToken: 'access-token', tokenType: 'Bearer', expiresAt: Date.now() + 3600_000, lastActiveAt, canRefresh: true, }, }; } return { success: true }; }); render( , ); await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith( '/api/auth/session/sync', expect.any(Object), )); expect(hostApiFetchMock.mock.calls.some(([path]) => path === '/api/auth/session/activity')).toBe(false); fireEvent.keyDown(window, { key: 'a' }); expect(hostApiFetchMock.mock.calls.some(([path]) => path === '/api/auth/session/activity')).toBe(false); }); 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: '在浏览器中继续' })).not.toBeInTheDocument(); unmount(); render( , ); expect(await screen.findByRole('button', { name: '在浏览器中继续' })).toBeInTheDocument(); }); it('opens the project conversation from its direct 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 module chooser at the default route without an active project', async () => { vi.stubGlobal('__NIANCODE_RENDERER_ONLY__', true); render( , ); expect(await screen.findByTestId('ai-module-selection-page')).toBeInTheDocument(); expect(screen.getByRole('heading', { name: '今天,想创作什么?' })).toBeInTheDocument(); }); it('keeps an uninitialized active project on project configuration from its direct route', 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', token_type: 'Bearer', expires_in: 43200, username: 'zhangsan', user_id: '1', }, session: { accessToken: 'access-token', tokenType: 'Bearer', expiresAt: Date.now() + 43_200_000, lastActiveAt: Date.now(), canRefresh: 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: '在浏览器中继续' })); 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', token_type: 'Bearer', expires_in: 43200, username: 'zhangsan', user_id: '1', }, session: { accessToken: 'fresh-access-token', tokenType: 'Bearer', expiresAt: Date.now() + 43_200_000, lastActiveAt: Date.now(), canRefresh: 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: '在浏览器中继续' })); await waitFor(() => { expect(hostApiFetchMock).toHaveBeenCalledWith('/api/provider-accounts/import-user-model-config', { method: 'POST', body: JSON.stringify({ accessToken: 'fresh-access-token', runtimeRefresh: 'apply', }), }); }); }); 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', token_type: 'Bearer', expires_in: 43200, username: 'zhangsan', user_id: '1', }, session: { accessToken: 'fresh-access-token', tokenType: 'Bearer', expiresAt: Date.now() + 43_200_000, lastActiveAt: Date.now(), canRefresh: true, }, }) .mockRejectedValueOnce(new Error('sync failed')) .mockResolvedValueOnce({ success: true }); render( } /> Module Selection} /> , ); fireEvent.click(screen.getByRole('button', { name: '在浏览器中继续' })); expect(await screen.findByText('登录失败,请稍后重试。')).toBeInTheDocument(); expect(screen.queryByText('Module Selection')).not.toBeInTheDocument(); expect(hostApiFetchMock).toHaveBeenCalledWith('/api/auth/logout', { method: 'POST', body: JSON.stringify({ accessToken: 'fresh-access-token' }), }); }); });