import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom'; import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { ModuleSelection } from '@/pages/ModuleSelection'; import { useAuthStore } from '@/stores/auth'; import { useUserProfileStore } from '@/stores/user-profile'; import { billingSummary, permanentBalance } from '../fixtures/permanent-points'; const api = vi.hoisted(() => ({ fetch: vi.fn() })); vi.mock('@/lib/host-api', () => ({ hostApiFetch: api.fetch })); let profileName: string; function profileResponse() { return { success: true, profile: { display_name: profileName, age: null, gender: null, avatar_url: null, share_age_with_agents: false, share_gender_with_agents: false, analysis_enabled: true, completed: true, version: 1, updated_at: '2026-10-13T00:00:00Z' } }; } function LocationProbe() { const location = useLocation(); return {location.pathname}; } function renderHome() { return render( } /> } /> ); } const writes = () => api.fetch.mock.calls.filter(([, init]) => init?.method && init.method !== 'GET'); beforeEach(() => { vi.resetAllMocks(); profileName = '小麦'; useAuthStore.setState({ initialized: true, loading: false, error: null, accessToken: 'home-fixture', tokenType: 'Bearer', expiresAt: Date.now() + 3600000, lastActiveAt: Date.now(), canRefresh: false, legacyRefreshToken: null, user: { username: 'child-wallet', userId: 'home-user', tenantId: null, deptId: null, authorities: [] }, moduleAccess: { programming: true, design: true, robot: true, cloud_agents: true } }); useUserProfileStore.setState({ profilesByUserId: {}, syncStatesByUserId: {} }); api.fetch.mockImplementation(async (path: string) => { if (path === '/api/works/user/agent-profile') return profileResponse(); if (path === '/api/works/billing/me') return { success: true, data: billingSummary() }; if (path.startsWith('/api/works/billing/recharge/orders?')) return { success: true, data: { items: [] } }; if (path === '/api/auth/logout') return { success: true }; throw new Error('Unexpected home account fixture request ' + path); }); }); afterEach(cleanup); it('shows the authenticated account and opens the real personal wallet with read requests only', async () => { renderHome(); const trigger = await screen.findByTestId('home-account-menu-trigger'); await waitFor(() => expect(trigger).toHaveTextContent('小麦')); expect(screen.getAllByTestId(/^ai-module-option-/)).toHaveLength(3); fireEvent.click(trigger); const menu = screen.getByTestId('home-account-menu'); expect(trigger).toHaveAttribute('aria-expanded', 'true'); expect(await within(menu).findByTestId('sidebar-total-token-points')).toHaveTextContent('75 点'); expect(within(menu).getByText('永久有效 · 预占 5 点', { exact: true })).toBeInTheDocument(); fireEvent.click(within(menu).getByRole('menuitem', { name: '词元点数与充值', exact: true })); const dialog = await screen.findByTestId('point-wallet-dialog'); await waitFor(() => expect(within(dialog).getByRole('button', { name: /500 点\s*¥10.00/ })).toBeEnabled()); expect(within(dialog).getByTestId('point-recharge-recipient')).toHaveTextContent('充值到:小麦(child-wallet)'); expect(api.fetch).toHaveBeenCalledWith('/api/works/billing/recharge/orders?offset=0', undefined); fireEvent.mouseDown(within(dialog).getByText('我的可用点数', { exact: true })); expect(trigger).toHaveAttribute('aria-expanded', 'false'); expect(dialog).toBeVisible(); expect(writes()).toEqual([]); }); it('opens the existing profile dialog and navigates to settings without saving profile changes', async () => { renderHome(); await screen.findByText('小麦,欢迎回来!'); fireEvent.click(screen.getByTestId('home-account-menu-trigger')); fireEvent.click(within(screen.getByTestId('home-account-menu')).getByRole('menuitem', { name: '个人资料', exact: true })); const profile = await screen.findByRole('dialog', { name: '个人资料', exact: true }); expect(within(profile).getByLabelText(/名字/)).toHaveValue('小麦'); fireEvent.click(within(profile).getByRole('button', { name: '关闭个人资料', exact: true })); fireEvent.click(screen.getByTestId('home-account-menu-trigger')); fireEvent.click(within(screen.getByTestId('home-account-menu')).getByRole('menuitem', { name: '设置', exact: true })); expect(await screen.findByTestId('location-path')).toHaveTextContent('/settings'); expect(writes()).toEqual([]); }); it('closes the menu on Escape and outside clicks while preserving the wallet instance', async () => { renderHome(); const trigger = await screen.findByTestId('home-account-menu-trigger'); fireEvent.click(trigger); const menu = screen.getByTestId('home-account-menu'); const wallet = await within(menu).findByTestId('sidebar-point-balance'); fireEvent.keyDown(document, { key: 'Escape' }); expect(trigger).toHaveAttribute('aria-expanded', 'false'); expect(menu).toHaveAttribute('aria-hidden', 'true'); fireEvent.click(trigger); fireEvent.mouseDown(screen.getByRole('heading', { name: '今天,想创作什么?', exact: true })); expect(trigger).toHaveAttribute('aria-expanded', 'false'); fireEvent.click(trigger); expect(within(menu).getByTestId('sidebar-point-balance')).toBe(wallet); expect(writes()).toEqual([]); }); it('remounts the wallet for another account and ignores the previous delayed balance', async () => { let finish!: (value: { success: boolean; data: ReturnType }) => void; const base = api.fetch.getMockImplementation()!; let firstBalance = true; api.fetch.mockImplementation((path: string, init) => { if (path === '/api/works/billing/me') { if (firstBalance) { firstBalance = false; return new Promise(resolve => { finish = resolve; }); } return Promise.resolve({ success: true, data: billingSummary({ own_points: permanentBalance({ total_remaining: '2.00' }) }) }); } return base(path, init); }); renderHome(); fireEvent.click(await screen.findByTestId('home-account-menu-trigger')); await waitFor(() => expect(firstBalance).toBe(false)); profileName = '小红'; await act(async () => useAuthStore.setState({ user: { username: 'second-wallet', userId: 'second-user', tenantId: null, deptId: null, authorities: [] } })); const trigger = screen.getByTestId('home-account-menu-trigger'); await waitFor(() => expect(trigger).toHaveTextContent('小红')); if (trigger.getAttribute('aria-expanded') !== 'true') fireEvent.click(trigger); await waitFor(() => expect(within(screen.getByTestId('home-account-menu')).getByTestId('sidebar-total-token-points')).toHaveTextContent('2 点')); await act(async () => finish({ success: true, data: billingSummary({ own_points: permanentBalance({ total_remaining: '999.00' }) }) })); expect(screen.queryByText('999 点', { exact: true })).not.toBeInTheDocument(); expect(writes()).toEqual([]); }); it('logs out only the fixture account and hides the home account entry for signed-out users', async () => { renderHome(); fireEvent.click(await screen.findByTestId('home-account-menu-trigger')); fireEvent.click(within(screen.getByTestId('home-account-menu')).getByRole('menuitem', { name: '退出登录', exact: true })); expect(await screen.findByTestId('location-path')).toHaveTextContent('/login'); expect(useAuthStore.getState().user).toBeNull(); expect(writes()).toEqual([['/api/auth/logout', expect.objectContaining({ method: 'POST' })]]); cleanup(); renderHome(); expect(screen.queryByTestId('home-account-menu-trigger')).not.toBeInTheDocument(); });