fix(home): restore account wallet and settings access
This commit is contained in:
1 parent
2b6ee37bdd
commit
5f7c851022
6 files changed
+373
-4
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
|
||||
import { billingSummary } from '../fixtures/permanent-points';
|
||||
|
||||
async function seedInactiveCodingProjects(resources: { rootDir: string; userDataDir: string }) {
|
||||
const now = '2026-09-06T08:00:00.000Z';
|
||||
@@ -50,6 +51,119 @@ async function seedInactiveCodingProjects(resources: { rootDir: string; userData
|
||||
}
|
||||
|
||||
test.describe('Makelore module navigation without setup flow', () => {
|
||||
test('keeps the home account wallet, profile, settings and logout available without creating a payment', async ({ launchElectronApp }, testInfo) => {
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
try {
|
||||
const page = await getStableWindow(app);
|
||||
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||
const applicationUrl = page.url();
|
||||
await page.goto('about:blank');
|
||||
await app.evaluate(({ ipcMain, BrowserWindow }, summary) => {
|
||||
BrowserWindow.getAllWindows()[0].setSize(1440, 900);
|
||||
const state = { loggedIn: true, requests: [] as { method: string; path: string }[] };
|
||||
Object.assign(globalThis, { homeAccountFixture: state });
|
||||
const result = (json: unknown) => ({ ok: true, data: { status: 200, ok: true, json } });
|
||||
ipcMain.removeHandler('hostapi:fetch');
|
||||
ipcMain.handle('hostapi:fetch', async (_event, request: { path?: string; method?: string }) => {
|
||||
const path = request.path ?? '';
|
||||
const method = (request.method ?? 'GET').toUpperCase();
|
||||
state.requests.push({ method, path });
|
||||
if (path === '/api/auth/session/sync') return result({ success: true, session: state.loggedIn ? { accessToken: 'home-account-fixture', tokenType: 'Bearer', expiresAt: Date.now() + 3600000, lastActiveAt: Date.now(), canRefresh: false } : null });
|
||||
if (path === '/api/auth/me') return result({ success: true, user: state.loggedIn ? { username: 'child-wallet', userId: 'home-user', tenantId: null, deptId: null, authorities: [] } : null, moduleAccess: { programming: true, design: true, robot: true, cloud_agents: true } });
|
||||
if (path === '/api/auth/logout' && method === 'POST') { state.loggedIn = false; return result({ success: true }); }
|
||||
if (path === '/api/works/user/agent-profile' && method === 'GET') return result({ success: true, profile: { display_name: '小麦', 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' } });
|
||||
if (path === '/api/works/billing/me' && method === 'GET') return result({ success: true, data: summary });
|
||||
if (path.startsWith('/api/works/billing/recharge/orders?') && method === 'GET') return result({ success: true, data: { items: [] } });
|
||||
if (path.startsWith('/api/works/billing/') || path === '/api/works/user/agent-profile') throw new Error('Unexpected home account write ' + method + ' ' + path);
|
||||
return result({ success: true });
|
||||
});
|
||||
}, billingSummary());
|
||||
// The setup-skipping fixture starts as a guest. Hydrate only this isolated
|
||||
// Renderer with a fake persisted session before the auth store is created.
|
||||
await page.addInitScript(() => {
|
||||
const now = Date.now();
|
||||
localStorage.setItem('niancode-auth', JSON.stringify({ version: 4, state: {
|
||||
authBase: 'https://biz.nianxx.cn/auth/', clientId: 'app',
|
||||
accessToken: 'home-account-fixture', tokenType: 'Bearer',
|
||||
expiresAt: now + 3600000, lastActiveAt: 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 },
|
||||
} }));
|
||||
// Setup-skipping mode also suppresses the home profile sync. Restore the
|
||||
// isolated account's cached profile in the store's persisted shape.
|
||||
localStorage.setItem('niancode-user-agent-profiles', JSON.stringify({ version: 1, state: {
|
||||
profilesByUserId: { 'home-user': { displayName: '小麦', age: null, gender: 'undisclosed', avatarUrl: null,
|
||||
shareAgeWithAgents: false, shareGenderWithAgents: false, analysisEnabled: true,
|
||||
pendingSync: false, version: 1, updatedAt: '2026-10-13T00:00:00Z' } },
|
||||
} }));
|
||||
});
|
||||
await page.goto(applicationUrl);
|
||||
const trigger = page.getByTestId('home-account-menu-trigger');
|
||||
const menu = page.getByTestId('home-account-menu');
|
||||
await expect(trigger).toContainText('小麦');
|
||||
await expect(page.locator('[data-testid^="ai-module-option-"]')).toHaveCount(3);
|
||||
const footer = await trigger.evaluate(element => element.closest('footer')!.getBoundingClientRect().top);
|
||||
const cards = await page.getByTestId('ai-module-options').boundingBox();
|
||||
expect(footer).toBeGreaterThanOrEqual(cards!.y + cards!.height);
|
||||
await trigger.click();
|
||||
await expect(menu.getByTestId('sidebar-total-token-points')).toHaveText('75 点');
|
||||
await expect(menu.getByText('永久有效 · 预占 5 点', { exact: true })).toBeVisible();
|
||||
await expect(menu.getByRole('menuitem', { name: '词元点数与充值', exact: true })).toBeVisible();
|
||||
await page.screenshot({ path: testInfo.outputPath('home-account-menu-desktop.png') });
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
||||
await expect(trigger).toBeFocused();
|
||||
await trigger.click();
|
||||
await page.getByRole('heading', { name: '今天,想创作什么?', exact: true }).click();
|
||||
await expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
||||
|
||||
await trigger.click();
|
||||
await menu.getByRole('menuitem', { name: '词元点数与充值', exact: true }).click();
|
||||
const wallet = page.getByTestId('point-wallet-dialog');
|
||||
await expect(wallet).toBeVisible();
|
||||
await expect(wallet.getByTestId('point-recharge-recipient')).toHaveText('充值到:小麦(child-wallet)');
|
||||
await expect(wallet.getByRole('button', { name: /500 点\s*¥10.00/ })).toBeEnabled();
|
||||
await wallet.getByText('我的可用点数', { exact: true }).click();
|
||||
await expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
||||
await expect(wallet).toBeVisible();
|
||||
await wallet.getByRole('button', { name: '关闭词元点数', exact: true }).click();
|
||||
await expect(wallet).toHaveCount(0);
|
||||
|
||||
await trigger.click();
|
||||
await menu.getByRole('menuitem', { name: '个人资料', exact: true }).click();
|
||||
const profile = page.getByRole('dialog', { name: '个人资料', exact: true });
|
||||
await expect(profile.getByLabel(/名字/)).toHaveValue('小麦');
|
||||
await profile.getByRole('button', { name: '关闭个人资料', exact: true }).click();
|
||||
await trigger.click();
|
||||
await menu.getByRole('menuitem', { name: '设置', exact: true }).click();
|
||||
await expect(page).toHaveURL(/\/settings$/);
|
||||
await page.evaluate(() => { window.location.hash = '#/module-select'; });
|
||||
await expect(trigger).toContainText('小麦');
|
||||
|
||||
await app.evaluate(({ BrowserWindow }) => { const window = BrowserWindow.getAllWindows()[0]; window.setMinimumSize(360, 600); window.setSize(375, 800); });
|
||||
await expect.poll(() => page.evaluate(() => innerWidth)).toBe(375);
|
||||
await trigger.click();
|
||||
await expect(menu.getByTestId('sidebar-total-token-points')).toHaveText('75 点');
|
||||
await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth - innerWidth)).toBeLessThanOrEqual(1);
|
||||
const menuBox = await menu.boundingBox();
|
||||
expect(menuBox!.x).toBeGreaterThanOrEqual(0);
|
||||
expect(menuBox!.x + menuBox!.width).toBeLessThanOrEqual(375);
|
||||
await page.screenshot({ path: testInfo.outputPath('home-account-menu-375.png') });
|
||||
await menu.getByRole('menuitem', { name: '退出登录', exact: true }).click();
|
||||
await expect(page).toHaveURL(/\/login$/);
|
||||
await expect(trigger).toHaveCount(0);
|
||||
const requests = await app.evaluate(() => (globalThis as typeof globalThis & { homeAccountFixture: { requests: { method: string; path: string }[] } }).homeAccountFixture.requests);
|
||||
const billingReads = requests.filter(request => request.path.startsWith('/api/works/billing/'));
|
||||
expect(billingReads.length).toBeGreaterThan(0);
|
||||
expect(billingReads.every(request => request.method === 'GET')).toBe(true);
|
||||
expect(requests.filter(request => request.path === '/api/works/user/agent-profile').every(request => request.method === 'GET')).toBe(true);
|
||||
expect(requests.filter(request => request.path === '/api/auth/logout' && request.method === 'POST')).toHaveLength(1);
|
||||
} finally {
|
||||
await app.evaluate(({ BrowserWindow }) => { for (const window of BrowserWindow.getAllWindows()) window.destroy(); }).catch(() => undefined);
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
|
||||
test('opens remembered Code projects and keeps the module return entry in chat instead of project settings', async ({
|
||||
electronTestResources,
|
||||
launchElectronApp,
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
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();
|
||||
});
|
||||
Reference in new issue
Block a user