Merge hidden recharge entry across sidebar and standalone account menus
This commit is contained in:
commit
5d500a932f
8 files changed
+45
-83
No files matched your search
@@ -174,7 +174,7 @@ test.describe('Makelore module navigation without setup flow', () => {
|
||||
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 expect(menu.getByRole('menuitem', { name: '词元点数与充值', exact: true })).toHaveCount(0);
|
||||
await checkAccountGeometry(1440);
|
||||
await page.screenshot({ path: testInfo.outputPath('home-account-menu-desktop.png') });
|
||||
await page.keyboard.press('Escape');
|
||||
@@ -184,17 +184,7 @@ test.describe('Makelore module navigation without setup flow', () => {
|
||||
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 expect(page.getByTestId('point-wallet-dialog')).toHaveCount(0);
|
||||
|
||||
await trigger.click();
|
||||
await menu.getByRole('menuitem', { name: '个人资料', exact: true }).click();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
|
||||
|
||||
type WalletFixture = { requests: string[]; paid: boolean; created: boolean };
|
||||
type WalletFixture = { requests: string[]; credited: boolean };
|
||||
type FixtureGlobal = typeof globalThis & { __pointWalletE2E: WalletFixture };
|
||||
|
||||
test.describe('Permanent point wallet', () => {
|
||||
test('shows the child recipient, reopens its frozen unpaid order, and refreshes only after confirmed credit', async ({ launchElectronApp }, testInfo) => {
|
||||
test('hides recharge while retaining account controls and refreshing the balance', async ({ launchElectronApp }, testInfo) => {
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
try {
|
||||
const page = await getStableWindow(app);
|
||||
@@ -12,21 +12,14 @@ test.describe('Permanent point wallet', () => {
|
||||
const applicationUrl = page.url();
|
||||
await page.goto('about:blank');
|
||||
await app.evaluate(({ ipcMain }) => {
|
||||
const state: WalletFixture = { requests: [], paid: false, created: false };
|
||||
const state: WalletFixture = { requests: [], credited: false };
|
||||
(globalThis as FixtureGlobal).__pointWalletE2E = state;
|
||||
const result = (json: unknown) => ({ ok: true, data: { status: 200, ok: true, json } });
|
||||
const points = () => ({
|
||||
total: state.paid ? '500.00' : '0.00', used: '0.00', reserved: '0.00',
|
||||
total_remaining: state.paid ? '500.00' : '0.00', entitlement_source: 'self',
|
||||
total: state.credited ? '500.00' : '0.00', used: '0.00', reserved: '0.00',
|
||||
total_remaining: state.credited ? '500.00' : '0.00', entitlement_source: 'self',
|
||||
family_shared: false, can_recharge: true, shared_available: null, expires_at: null,
|
||||
});
|
||||
const order = () => ({
|
||||
id: 'order-e2e', product_id: 'product-e2e', product_name: '500 词元点数',
|
||||
status: state.paid ? 'succeeded' : 'pending', amount_cents: 1000, point_amount: '500.00',
|
||||
currency: 'CNY', created_at: '2026-09-22T00:00:00Z',
|
||||
paid_at: state.paid ? '2026-09-22T01:00:00Z' : null, manual_review_required: false,
|
||||
qr_payload: state.paid ? null : 'weixin://wxpay/e2e-fixture-not-a-real-payment',
|
||||
});
|
||||
ipcMain.removeHandler('hostapi:fetch');
|
||||
ipcMain.handle('hostapi:fetch', async (_event, request: { path?: string; method?: string }) => {
|
||||
const path = request.path ?? '';
|
||||
@@ -55,23 +48,9 @@ test.describe('Permanent point wallet', () => {
|
||||
can_recharge: true, payment_configured: true, signup_bonus: '100.00', points_per_yuan: 50,
|
||||
recharge_products: [{
|
||||
id: 'product-e2e', name: '词元点数',
|
||||
amount_cents: state.created ? 2000 : 1000, point_amount: state.created ? '1000.00' : '500.00', currency: 'CNY',
|
||||
amount_cents: 1000, point_amount: '500.00', currency: 'CNY',
|
||||
}],
|
||||
} });
|
||||
if (path.startsWith('/api/works/billing/recharge/orders?')) return result({
|
||||
success: true, data: { items: state.created ? [order()] : [] },
|
||||
});
|
||||
if (path === '/api/works/billing/recharge/orders' && method === 'POST') {
|
||||
state.created = true;
|
||||
return result({ success: true, data: order() });
|
||||
}
|
||||
if (path === '/api/works/billing/recharge/orders/order-e2e') return result({ success: true, data: order() });
|
||||
if (path.startsWith('/api/works/billing/points/transactions?')) return result({
|
||||
success: true, data: { has_more: false, items: state.paid ? [{
|
||||
id: 'credit-e2e', kind: 'credit', description: '充值', status: 'credited',
|
||||
points: '500.00', reserved_points: '0.00', created_at: '2026-09-22T01:00:00Z',
|
||||
}] : [] },
|
||||
});
|
||||
return { ok: false, error: { message: 'Unexpected fixture request: ' + method + ' ' + path } };
|
||||
});
|
||||
});
|
||||
@@ -82,36 +61,29 @@ test.describe('Permanent point wallet', () => {
|
||||
await page.getByTestId('sidebar-member-menu-trigger').click();
|
||||
await expect(page.getByTestId('sidebar-total-token-points')).toHaveText('0 点');
|
||||
await expect(page.getByText('点数已用尽,请由家长完成当前账号充值。')).toBeVisible();
|
||||
await page.getByTestId('sidebar-point-wallet-open').click();
|
||||
const wallet = page.getByTestId('point-wallet-dialog');
|
||||
await expect(wallet).toContainText('1 元充值 50 点');
|
||||
await expect(wallet.getByTestId('point-recharge-recipient')).toHaveText('充值到:小麦(child-wallet)');
|
||||
await expect(wallet).toContainText('请由家长登录孩子的账号完成付款');
|
||||
await expect(wallet).not.toContainText('重置卡');
|
||||
await wallet.getByRole('button', { name: /500 点\s*¥10.00/ }).click();
|
||||
await expect(wallet.getByAltText('充值支付二维码')).toBeVisible();
|
||||
await expect(page.getByTestId('point-recharge-checkout')).toContainText('¥10.00 · 500 点');
|
||||
await expect(wallet.getByRole('button', { name: /1,000 点\s*¥20.00/ })).toHaveCount(0);
|
||||
await page.screenshot({ path: testInfo.outputPath('pending-permanent-points.png') });
|
||||
const menu = page.getByTestId('sidebar-account-menu');
|
||||
await expect(menu.getByRole('menuitem', { name: '词元点数与充值' })).toHaveCount(0);
|
||||
await expect(page.getByTestId('sidebar-point-wallet-open')).toHaveCount(0);
|
||||
await expect(page.getByTestId('point-wallet-dialog')).toHaveCount(0);
|
||||
await expect(menu.getByRole('menuitem', { name: '个人资料' })).toBeVisible();
|
||||
await expect(menu.getByRole('menuitem', { name: '设置' })).toBeVisible();
|
||||
await expect(menu.getByRole('menuitem', { name: '退出登录' })).toBeVisible();
|
||||
await page.screenshot({ path: testInfo.outputPath('account-menu-recharge-hidden.png') });
|
||||
|
||||
await page.getByTestId('sidebar-member-menu-trigger').click();
|
||||
await app.evaluate(() => { (globalThis as FixtureGlobal).__pointWalletE2E.credited = true; });
|
||||
await page.getByTestId('sidebar-member-menu-trigger').click();
|
||||
await expect(page.getByTestId('sidebar-total-token-points')).toHaveText('500 点');
|
||||
await expect(page.getByText('点数已用尽,请由家长完成当前账号充值。')).toHaveCount(0);
|
||||
await expect(page.getByTestId('sidebar-point-wallet-open')).toHaveCount(0);
|
||||
|
||||
// Reloading the Renderer must recover the existing server order using GET only.
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.getByTestId('sidebar-member-menu-trigger').click();
|
||||
await page.getByTestId('sidebar-point-wallet-open').click();
|
||||
await wallet.getByRole('button', { name: '查看原订单' }).click();
|
||||
await expect(wallet.getByAltText('充值支付二维码')).toBeVisible();
|
||||
await app.evaluate(() => { (globalThis as FixtureGlobal).__pointWalletE2E.paid = true; });
|
||||
await wallet.getByRole('button', { name: '刷新此订单' }).click();
|
||||
await expect(page.getByTestId('point-recharge-checkout')).toContainText('已到账');
|
||||
await expect(wallet.getByAltText('充值支付二维码')).toHaveCount(0);
|
||||
await expect(wallet).toContainText('500 点');
|
||||
await wallet.getByRole('tab', { name: '收支记录' }).click();
|
||||
await expect(wallet).toContainText('+500 点');
|
||||
await expect(wallet).toContainText('已入账');
|
||||
await page.screenshot({ path: testInfo.outputPath('credited-permanent-points.png') });
|
||||
await expect(page.getByTestId('sidebar-total-token-points')).toHaveText('500 点');
|
||||
await expect(page.getByTestId('sidebar-point-wallet-open')).toHaveCount(0);
|
||||
expect(await app.evaluate(() => (globalThis as FixtureGlobal).__pointWalletE2E.requests.filter(
|
||||
(request) => request === 'POST /api/works/billing/recharge/orders',
|
||||
).length)).toBe(1);
|
||||
(request) => request.includes('/api/works/billing/recharge/') || request.includes('/api/works/billing/points/transactions'),
|
||||
))).toEqual([]);
|
||||
} catch (error) {
|
||||
const page = app.windows().at(-1);
|
||||
if (page && !page.isClosed()) {
|
||||
|
||||
@@ -46,7 +46,7 @@ beforeEach(() => {
|
||||
});
|
||||
afterEach(cleanup);
|
||||
|
||||
it('shows the authenticated account and opens the real personal wallet with read requests only', async () => {
|
||||
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('小麦'));
|
||||
@@ -56,14 +56,10 @@ it('shows the authenticated account and opens the real personal wallet with read
|
||||
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(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([]);
|
||||
});
|
||||
|
||||
|
||||
@@ -85,13 +85,17 @@ describe('Sidebar permanent point wallet', () => {
|
||||
fetchBillingSummaryMock.mockReset();
|
||||
fetchBillingSummaryMock.mockResolvedValue(billingSummary());
|
||||
});
|
||||
it('opens the point wallet from the account menu without retired member controls', async () => {
|
||||
it('keeps the balance and account controls while hiding the recharge entry', async () => {
|
||||
await openAccount();
|
||||
await waitFor(() => expect(screen.getByTestId('sidebar-total-token-points')).toHaveTextContent('75 点'));
|
||||
expect(screen.queryByText(/重置卡|升级订阅|会员等级|本周剩余/)).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByTestId('sidebar-point-wallet-open'));
|
||||
expect(screen.getByTestId('point-wallet-dialog')).toHaveTextContent('1 元充值 50 点');
|
||||
expect(screen.getByTestId('point-recharge-recipient')).toHaveTextContent('测试用户(member@example.com)');
|
||||
expect(screen.getByTestId('sidebar-point-balance')).toHaveTextContent('永久有效 · 预占 5 点');
|
||||
expect(screen.queryByTestId('sidebar-point-wallet-open')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('menuitem', { name: '词元点数与充值' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('point-wallet-dialog')).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('menuitem', { name: '个人资料' })).toBeVisible();
|
||||
expect(screen.getByRole('menuitem', { name: '设置' })).toBeVisible();
|
||||
expect(screen.getByRole('menuitem', { name: '退出登录' })).toBeVisible();
|
||||
});
|
||||
it('shows a retryable failure without inventing a zero balance', async () => {
|
||||
fetchBillingSummaryMock.mockRejectedValue(new Error('offline'));
|
||||
|
||||
Reference in new issue
Block a user