Files
makelore/tests/unit/home-account-menu.test.tsx
T

150 lines
8.8 KiB
TypeScript

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(), info: vi.fn() }));
vi.mock('@/lib/host-api', () => ({ hostApiFetch: api.fetch }));
vi.mock('sonner', () => ({ toast: { info: api.info } }));
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 <><output data-testid="location-path">{location.pathname}</output>
<output data-testid="location-state">{JSON.stringify(location.state)}</output></>;
}
function renderHome() {
return render(<MemoryRouter initialEntries={['/module-select']}><Routes>
<Route path="/module-select" element={<ModuleSelection />} />
<Route path="*" element={<LocationProbe />} />
</Routes></MemoryRouter>);
}
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 balance and controls without a recharge entry', 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();
expect(within(menu).queryByRole('menuitem', { name: '词元点数与充值', exact: true })).not.toBeInTheDocument();
expect(screen.queryByTestId('point-wallet-dialog')).not.toBeInTheDocument();
for (const name of ['个人资料', '设置', '退出登录']) expect(within(menu).getByRole('menuitem', { name, exact: true })).toBeVisible();
expect(api.fetch.mock.calls.filter(([path]) => path.includes('/billing/recharge/') || path.includes('/billing/points/transactions'))).toEqual([]);
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(screen.getByTestId('location-state')).toHaveTextContent('"settingsReturnTo":"/module-select"');
expect(writes()).toEqual([]);
});
it('keeps the current page and inputs when its unsaved-content guard rejects opening settings', async () => {
renderHome();
await screen.findByText('小麦,欢迎回来!');
fireEvent.click(screen.getByTestId('home-account-menu-trigger'));
const preventLeaving = (event: Event) => event.preventDefault();
window.addEventListener('beforeunload', preventLeaving);
try {
fireEvent.click(within(screen.getByTestId('home-account-menu')).getByRole('menuitem', { name: '设置', exact: true }));
expect(screen.getByTestId('ai-module-selection-page')).toBeInTheDocument();
expect(screen.getByTestId('home-account-menu-trigger')).toHaveAttribute('aria-expanded', 'true');
expect(screen.queryByTestId('location-path')).not.toBeInTheDocument();
expect(api.info).toHaveBeenCalledWith('请先保存当前修改,再打开设置。');
expect(writes()).toEqual([]);
} finally { window.removeEventListener('beforeunload', preventLeaving); }
});
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<typeof billingSummary> }) => 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();
});