feat: replace desktop membership with permanent point wallet
This commit is contained in:
128
tests/e2e/permanent-point-wallet.spec.ts
Normal file
128
tests/e2e/permanent-point-wallet.spec.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
|
||||
|
||||
type WalletFixture = { requests: string[]; paid: boolean; created: boolean };
|
||||
type FixtureGlobal = typeof globalThis & { __pointWalletE2E: WalletFixture };
|
||||
|
||||
test.describe('Permanent point wallet', () => {
|
||||
test('reopens an unpaid order, keeps its frozen amount, and refreshes only after confirmed credit', 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 }) => {
|
||||
const state: WalletFixture = { requests: [], paid: false, created: 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',
|
||||
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 ?? '';
|
||||
const method = (request.method ?? 'GET').toUpperCase();
|
||||
state.requests.push(method + ' ' + path);
|
||||
if (path === '/api/auth/session/sync') return result({
|
||||
success: true, session: {
|
||||
accessToken: 'point-wallet-fixture-token', tokenType: 'Bearer',
|
||||
expiresAt: Date.now() + 3_600_000, lastActiveAt: Date.now(), canRefresh: false,
|
||||
},
|
||||
});
|
||||
if (path === '/api/auth/me') return result({
|
||||
success: true,
|
||||
user: { username: 'point-wallet', userId: 'point-wallet-user', tenantId: null, deptId: null, authorities: [] },
|
||||
moduleAccess: { programming: true, design: true, robot: true },
|
||||
});
|
||||
if (path === '/api/works/user/agent-profile') 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-09-22T00:00:00Z',
|
||||
},
|
||||
});
|
||||
if (path === '/api/works/billing/me') return result({ success: true, data: {
|
||||
own_points: points(), points: points(), funding_error: null,
|
||||
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',
|
||||
}],
|
||||
} });
|
||||
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 } };
|
||||
});
|
||||
});
|
||||
await page.goto(applicationUrl, { waitUntil: 'domcontentloaded' });
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await expect(page.getByTestId('main-layout')).toBeVisible();
|
||||
await expect(page.getByTestId('sidebar-member-menu-trigger')).toContainText('词元点数用户');
|
||||
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).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') });
|
||||
|
||||
// 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') });
|
||||
expect(await app.evaluate(() => (globalThis as FixtureGlobal).__pointWalletE2E.requests.filter(
|
||||
(request) => request === 'POST /api/works/billing/recharge/orders',
|
||||
).length)).toBe(1);
|
||||
} catch (error) {
|
||||
const page = app.windows().at(-1);
|
||||
if (page && !page.isClosed()) {
|
||||
await page.screenshot({ path: testInfo.outputPath('wallet-failure.png') });
|
||||
await testInfo.attach('wallet-failure-dom', { body: await page.locator('body').innerText(), contentType: 'text/plain' });
|
||||
}
|
||||
await testInfo.attach('wallet-fixture-requests', {
|
||||
body: JSON.stringify(await app.evaluate(() => (globalThis as FixtureGlobal).__pointWalletE2E.requests)),
|
||||
contentType: 'application/json',
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,126 +0,0 @@
|
||||
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
|
||||
|
||||
test.describe('Account reset-card wallet', () => {
|
||||
test('hides a redeemed card and refreshes the authoritative point balance', 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 }) => {
|
||||
const requests: string[] = [];
|
||||
let redeemed = false;
|
||||
const result = (json: unknown) => ({ ok: true, data: { status: 200, ok: true, json } });
|
||||
const points = () => ({
|
||||
plan_code: 'mastery',
|
||||
plan_name: '精通',
|
||||
cycle_start: '2026-09-08T00:00:00Z',
|
||||
cycle_end: '2026-09-15T00:00:00Z',
|
||||
next_refresh_at: '2026-09-15T00:00:00Z',
|
||||
weekly_allowance: '500.00',
|
||||
weekly_used: redeemed ? '0.00' : '400.00',
|
||||
weekly_reserved: '0.00',
|
||||
weekly_remaining: redeemed ? '500.00' : '100.00',
|
||||
permanent_total: '50.00',
|
||||
permanent_used: '0.00',
|
||||
permanent_reserved: '0.00',
|
||||
permanent_remaining: '50.00',
|
||||
total_remaining: redeemed ? '550.00' : '150.00',
|
||||
entitlement_source: 'self',
|
||||
family_shared: false,
|
||||
can_manage_membership: true,
|
||||
upgrade_action: 'self_service',
|
||||
shared_available: null,
|
||||
});
|
||||
const card = () => ({
|
||||
id: 'card-e2e',
|
||||
status: redeemed ? 'redeemed' : 'available',
|
||||
granted_at: '2026-09-08T00:00:00Z',
|
||||
expires_at: '2099-09-15T00:00:00Z',
|
||||
redeemed_at: redeemed ? '2026-09-09T00:00:00Z' : null,
|
||||
redeemed_cycle_id: redeemed ? 'cycle-e2e' : null,
|
||||
});
|
||||
(globalThis as typeof globalThis & { __resetCardE2E?: { requests: string[] } }).__resetCardE2E = { requests };
|
||||
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();
|
||||
requests.push(`${method} ${path}`);
|
||||
if (path === '/api/auth/session/sync') return result({
|
||||
success: true,
|
||||
session: {
|
||||
accessToken: 'reset-card-e2e-token',
|
||||
tokenType: 'Bearer',
|
||||
expiresAt: Date.now() + 3_600_000,
|
||||
lastActiveAt: Date.now(),
|
||||
canRefresh: false,
|
||||
},
|
||||
});
|
||||
if (path === '/api/auth/me') return result({
|
||||
success: true,
|
||||
user: {
|
||||
username: 'reset-card-e2e',
|
||||
userId: 'reset-card-e2e-user',
|
||||
tenantId: null,
|
||||
deptId: null,
|
||||
authorities: [],
|
||||
},
|
||||
moduleAccess: { programming: true, design: true, robot: true },
|
||||
});
|
||||
if (path === '/api/works/user/agent-profile') 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-09-08T00:00:00Z',
|
||||
},
|
||||
});
|
||||
if (path === '/api/works/billing/points') return result({ success: true, points: points() });
|
||||
if (path === '/api/works/billing/reset-cards' && method === 'GET') {
|
||||
return result({ success: true, cards: [card()] });
|
||||
}
|
||||
if (path === '/api/works/billing/reset-cards/card-e2e/redeem' && method === 'POST') {
|
||||
redeemed = true;
|
||||
return result({ success: true, card: card() });
|
||||
}
|
||||
return { ok: false, error: { message: `Unexpected Host API request: ${method} ${path}` } };
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto(applicationUrl, { waitUntil: 'domcontentloaded' });
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await expect(page.getByTestId('main-layout')).toBeVisible();
|
||||
await page.getByTestId('sidebar-member-menu-trigger').click();
|
||||
await page.getByTestId('sidebar-reset-cards-menuitem').click();
|
||||
|
||||
await expect.poll(async () => app.evaluate(() => (
|
||||
(globalThis as typeof globalThis & { __resetCardE2E?: { requests: string[] } }).__resetCardE2E?.requests
|
||||
.filter((request) => request === 'GET /api/works/billing/reset-cards').length ?? 0
|
||||
))).toBeGreaterThan(0);
|
||||
await expect(page.getByTestId('sidebar-reset-card-card-e2e')).toContainText('可使用');
|
||||
await expect(page.getByTestId('sidebar-reset-card-card-e2e')).toContainText('有效期至 2099-09-15');
|
||||
await page.getByTestId('sidebar-reset-card-redeem-card-e2e').click();
|
||||
await expect(page.getByTestId('sidebar-reset-card-card-e2e')).toHaveCount(0);
|
||||
await expect(page.getByTestId('sidebar-reset-cards-drawer')).toContainText('暂无未使用的重置卡。');
|
||||
await expect(page.getByTestId('sidebar-reset-card-available-count')).toHaveCount(0);
|
||||
await page.screenshot({ path: testInfo.outputPath('used-reset-card-hidden.png') });
|
||||
|
||||
await page.getByTestId('sidebar-account-usage-menuitem').click();
|
||||
await expect(page.getByTestId('sidebar-weekly-token-points')).toContainText('500 / 500 点');
|
||||
await expect.poll(async () => app.evaluate(() => (
|
||||
(globalThis as typeof globalThis & { __resetCardE2E?: { requests: string[] } }).__resetCardE2E?.requests
|
||||
.filter((request) => request === 'POST /api/works/billing/reset-cards/card-e2e/redeem').length ?? 0
|
||||
))).toBe(1);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user