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

135 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 <output data-testid="location-path">{location.pathname}</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 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<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();
});