Consolidate approved trial client updates and preserve composer runtime
This commit is contained in:
1 parent
154a21a295
commit
020b36673d
32 files changed
+3668
-947
No files matched your search
@@ -1,6 +1,8 @@
|
||||
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
|
||||
|
||||
test('guides an empty channel workspace from the sidebar back to My Agents', async ({ launchElectronApp }, testInfo) => {
|
||||
test('guides an empty channel workspace from the sidebar back to My Agents', async ({
|
||||
launchElectronApp,
|
||||
}, testInfo) => {
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
try {
|
||||
const page = await getStableWindow(app);
|
||||
@@ -13,19 +15,61 @@ test('guides an empty channel workspace from the sidebar back to My Agents', asy
|
||||
Object.assign(globalThis, { cloudChannelEmptyFixtureCalls: calls });
|
||||
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; body?: string }) => {
|
||||
const path = request.path ?? '';
|
||||
if (path === '/api/auth/session/sync') return result({ success: true, session: { accessToken: 'ui-fixture', tokenType: 'Bearer', expiresAt: Date.now() + 3600000, lastActiveAt: Date.now(), canRefresh: false } });
|
||||
if (path === '/api/auth/me') return result({ success: true, user: { username: 'creator', userId: 'creator', tenantId: null, deptId: null, authorities: [] }, moduleAccess: { programming: true, design: true, robot: true, cloud_agents: 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-10-13T00:00:00Z' } });
|
||||
if (!path.startsWith('/api/cloud-agents/')) return result({ success: true });
|
||||
const body = typeof request.body === 'string' ? JSON.parse(request.body) : request.body ?? {};
|
||||
calls.push({ method: request.method ?? 'GET', path, operation: body.operation });
|
||||
if (path.endsWith('/recovery')) return result({ recent: null, pending: [] });
|
||||
if (path.endsWith('/agents') || path.endsWith('/bootstrap')) return result({ agents: [], next_cursor: null });
|
||||
if (path.endsWith('/actions') && body.operation === 'channelAccounts') return result({ items: [] });
|
||||
throw new Error('Unexpected empty channel fixture request ' + path);
|
||||
});
|
||||
ipcMain.handle(
|
||||
'hostapi:fetch',
|
||||
async (_event, request: { path?: string; method?: string; body?: string }) => {
|
||||
const path = request.path ?? '';
|
||||
if (path === '/api/auth/session/sync')
|
||||
return result({
|
||||
success: true,
|
||||
session: {
|
||||
accessToken: 'ui-fixture',
|
||||
tokenType: 'Bearer',
|
||||
expiresAt: Date.now() + 3600000,
|
||||
lastActiveAt: Date.now(),
|
||||
canRefresh: false,
|
||||
},
|
||||
});
|
||||
if (path === '/api/auth/me')
|
||||
return result({
|
||||
success: true,
|
||||
user: {
|
||||
username: 'creator',
|
||||
userId: 'creator',
|
||||
tenantId: null,
|
||||
deptId: null,
|
||||
authorities: [],
|
||||
},
|
||||
moduleAccess: { programming: true, design: true, robot: true, cloud_agents: 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-10-13T00:00:00Z',
|
||||
},
|
||||
});
|
||||
if (!path.startsWith('/api/cloud-agents/')) return result({ success: true });
|
||||
const body =
|
||||
typeof request.body === 'string' ? JSON.parse(request.body) : (request.body ?? {});
|
||||
calls.push({ method: request.method ?? 'GET', path, operation: body.operation });
|
||||
if (path.endsWith('/recovery')) return result({ recent: null, pending: [] });
|
||||
if (path.endsWith('/agents') || path.endsWith('/bootstrap'))
|
||||
return result({ agents: [], next_cursor: null });
|
||||
if (path.endsWith('/actions') && body.operation === 'channelAccounts')
|
||||
return result({ items: [] });
|
||||
throw new Error('Unexpected empty channel fixture request ' + path);
|
||||
}
|
||||
);
|
||||
});
|
||||
await page.goto(applicationUrl);
|
||||
await page.getByTestId('ai-module-option-cloud_agents').click();
|
||||
@@ -34,36 +78,70 @@ test('guides an empty channel workspace from the sidebar back to My Agents', asy
|
||||
await expect(page).toHaveURL(/\/cloud-agents\/channels$/);
|
||||
const hint = page.getByRole('heading', { name: '还没有可连接的智能体', exact: true });
|
||||
await expect(hint).toBeVisible();
|
||||
await expect(page.getByText('先创建一个智能体,配置完成后点击“开始使用”,就能连接到微信。', { exact: true })).toBeVisible();
|
||||
await expect(
|
||||
page.getByText('先创建一个智能体,配置完成后点击“开始使用”,就能连接到微信。', { exact: true })
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: '添加第一个微信', exact: true })).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: '添加微信', exact: true })).toBeEnabled();
|
||||
await page.screenshot({ path: testInfo.outputPath('wechat-no-agents-desktop.png'), fullPage: true });
|
||||
|
||||
await page.getByRole('button', { name: '添加微信', exact: true }).click();
|
||||
const dialog = page.getByRole('dialog', { name: '添加微信', exact: true });
|
||||
await expect(dialog.getByRole('heading', { name: '还没有可连接的智能体', exact: true })).toBeVisible();
|
||||
await expect(
|
||||
dialog.getByRole('heading', { name: '还没有可连接的智能体', exact: true })
|
||||
).toBeVisible();
|
||||
await expect(dialog.getByRole('button', { name: '下一步:扫码连接', exact: true })).toBeDisabled();
|
||||
await dialog.getByRole('button', { name: '关闭', exact: true }).click();
|
||||
|
||||
await app.evaluate(({ BrowserWindow }) => { const window = BrowserWindow.getAllWindows()[0]; window.setMinimumSize(800, 600); window.setSize(980, 680); });
|
||||
await app.evaluate(({ BrowserWindow }) => {
|
||||
const window = BrowserWindow.getAllWindows()[0];
|
||||
window.setMinimumSize(800, 600);
|
||||
window.setSize(980, 680);
|
||||
});
|
||||
await expect(hint).toBeVisible();
|
||||
await expect.poll(() => page.getByTestId('main-content').evaluate(element => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(1);
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.getByTestId('main-content').evaluate((element) => element.scrollWidth - element.clientWidth)
|
||||
)
|
||||
.toBeLessThanOrEqual(1);
|
||||
await page.screenshot({ path: testInfo.outputPath('wechat-no-agents-compact.png'), fullPage: true });
|
||||
await page.getByRole('button', { name: '去我的智能体', exact: true }).click();
|
||||
await expect(page).toHaveURL(/\/cloud-agents\?view=mine$/);
|
||||
await expect(page.getByTestId('cloud-agents-page')).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: '做一个属于你的 AI 助手吧', exact: true })).toBeVisible();
|
||||
await expect(sidebar.getByRole('button', { name: '我的智能体', exact: true })).toHaveAttribute('aria-current', 'page');
|
||||
const calls = await app.evaluate(() => (globalThis as typeof globalThis & { cloudChannelEmptyFixtureCalls: { method: string; operation?: string }[] }).cloudChannelEmptyFixtureCalls);
|
||||
expect(calls.filter(call => call.operation).map(call => call.operation)).toEqual(['channelAccounts']);
|
||||
expect(calls.every(call => call.method === 'GET' || call.operation === 'channelAccounts')).toBe(true);
|
||||
await expect(
|
||||
page.getByRole('heading', { name: '做一个属于你的 AI 助手吧', exact: true })
|
||||
).toBeVisible();
|
||||
await expect(sidebar.getByRole('button', { name: '我的智能体', exact: true })).toHaveAttribute(
|
||||
'aria-current',
|
||||
'page'
|
||||
);
|
||||
const calls = await app.evaluate(
|
||||
() =>
|
||||
(
|
||||
globalThis as typeof globalThis & {
|
||||
cloudChannelEmptyFixtureCalls: { method: string; operation?: string }[];
|
||||
}
|
||||
).cloudChannelEmptyFixtureCalls
|
||||
);
|
||||
expect(calls.filter((call) => call.operation).map((call) => call.operation)).toEqual([
|
||||
'channelAccounts',
|
||||
]);
|
||||
expect(calls.every((call) => call.method === 'GET' || call.operation === 'channelAccounts')).toBe(
|
||||
true
|
||||
);
|
||||
} finally {
|
||||
await app.evaluate(({ BrowserWindow }) => { for (const window of BrowserWindow.getAllWindows()) window.destroy(); }).catch(() => undefined);
|
||||
await app
|
||||
.evaluate(({ BrowserWindow }) => {
|
||||
for (const window of BrowserWindow.getAllWindows()) window.destroy();
|
||||
})
|
||||
.catch(() => undefined);
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
|
||||
test('WeChat binding cards connect an account and switch its Agent through the channel workspace', async ({ launchElectronApp }, testInfo) => {
|
||||
test('WeChat binding cards connect an account and switch its Agent through the channel workspace', async ({
|
||||
launchElectronApp,
|
||||
}, testInfo) => {
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
const agentSlug = 'ml-' + 'a'.repeat(32);
|
||||
const secondAgentSlug = 'ml-' + 'b'.repeat(32);
|
||||
@@ -77,69 +155,319 @@ test('WeChat binding cards connect an account and switch its Agent through the c
|
||||
const calls: { operation: string; input: unknown }[] = [];
|
||||
Object.assign(globalThis, { cloudChannelFixtureCalls: calls });
|
||||
let verified = false;
|
||||
let rejectUnbind = true;
|
||||
const slug = 'ml-' + 'a'.repeat(32);
|
||||
const configuration = { model: 'test-model', tools: [], knowledges: [], mcps: [], skills: [], preload_skills: [], subagents: [], tool_approval_mode: 'default', max_execution_steps: 40, max_output_tokens: 4096, max_run_seconds: 600 };
|
||||
const draft = { slug, name: '我的创作助手', purpose: '整理选题与制作演示文稿', system_prompt: '使用简洁自然的中文', draft_revision: 2, updated_at: '2026-09-13T00:00:00Z', configuration, published_version: 1, enabled: true, archived: false };
|
||||
const configuration = {
|
||||
model: 'test-model',
|
||||
tools: [],
|
||||
knowledges: [],
|
||||
mcps: [],
|
||||
skills: [],
|
||||
preload_skills: [],
|
||||
subagents: [],
|
||||
tool_approval_mode: 'default',
|
||||
max_execution_steps: 40,
|
||||
max_output_tokens: 4096,
|
||||
max_run_seconds: 600,
|
||||
};
|
||||
const draft = {
|
||||
slug,
|
||||
name: '我的创作助手',
|
||||
purpose: '整理选题与制作演示文稿',
|
||||
system_prompt: '使用简洁自然的中文',
|
||||
draft_revision: 2,
|
||||
updated_at: '2026-09-13T00:00:00Z',
|
||||
configuration,
|
||||
published_version: 1,
|
||||
enabled: true,
|
||||
archived: false,
|
||||
};
|
||||
const secondSlug = 'ml-' + 'b'.repeat(32);
|
||||
const secondDraft = { ...draft, slug: secondSlug, name: '资料整理助手', draft_revision: 1, published_version: 1 };
|
||||
const channel = { id: 'wechat-a', address: 'wechat:personal', display_name: '创作微信', target_agent_address: 'yuxi:makelore:' + slug, route_revision: 3, provider_generation: 'generation-a', binding_id: 'binding-a', enabled: true, status: 'active', worker_online: true, agent_slug: slug, published_version: 1, desired_state: 'enabled', sync_state: 'ready', health: 'connected', last_confirmed_at: '2026-09-13T01:00:00Z', revision: 3, blockers: [], access_mode: 'self_only', policy_revision: 0, target_agent_slug: slug, target_agent_name: draft.name, target_agent_published_version: 1 };
|
||||
const spare = { id: 'wechat-b', address: 'wechat:spare', display_name: '备用微信', target_agent_address: null, route_revision: 1, provider_generation: 'generation-b', binding_id: 'binding-b', enabled: false, status: 'paused', worker_online: true, agent_slug: null, published_version: null, desired_state: 'disabled', sync_state: 'ready', health: 'login_required', last_confirmed_at: null, revision: 1, blockers: ['target_required'], access_mode: 'self_only', policy_revision: 0, target_agent_slug: null, target_agent_name: null, target_agent_published_version: null };
|
||||
const secondDraft = {
|
||||
...draft,
|
||||
slug: secondSlug,
|
||||
name: '资料整理助手',
|
||||
draft_revision: 1,
|
||||
published_version: 1,
|
||||
};
|
||||
const channel = {
|
||||
id: 'wechat-a',
|
||||
address: 'wechat:personal',
|
||||
display_name: '创作微信',
|
||||
target_agent_address: 'yuxi:makelore:' + slug,
|
||||
route_revision: 3,
|
||||
provider_generation: 'generation-a',
|
||||
binding_id: 'binding-a',
|
||||
enabled: true,
|
||||
status: 'active',
|
||||
worker_online: true,
|
||||
agent_slug: slug,
|
||||
published_version: 1,
|
||||
desired_state: 'enabled',
|
||||
sync_state: 'ready',
|
||||
health: 'connected',
|
||||
last_confirmed_at: '2026-09-13T01:00:00Z',
|
||||
revision: 3,
|
||||
blockers: [],
|
||||
access_mode: 'self_only',
|
||||
policy_revision: 0,
|
||||
target_agent_slug: slug,
|
||||
target_agent_name: draft.name,
|
||||
target_agent_published_version: 1,
|
||||
};
|
||||
const spare = {
|
||||
id: 'wechat-b',
|
||||
address: 'wechat:spare',
|
||||
display_name: '备用微信',
|
||||
target_agent_address: null,
|
||||
route_revision: 1,
|
||||
provider_generation: 'generation-b',
|
||||
binding_id: 'binding-b',
|
||||
enabled: false,
|
||||
status: 'paused',
|
||||
worker_online: true,
|
||||
agent_slug: null,
|
||||
published_version: null,
|
||||
desired_state: 'disabled',
|
||||
sync_state: 'ready',
|
||||
health: 'login_required',
|
||||
last_confirmed_at: null,
|
||||
revision: 1,
|
||||
blockers: ['target_required'],
|
||||
access_mode: 'self_only',
|
||||
policy_revision: 0,
|
||||
target_agent_slug: null,
|
||||
target_agent_name: null,
|
||||
target_agent_published_version: null,
|
||||
};
|
||||
let channelAccounts = [channel, spare];
|
||||
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; body?: string }) => {
|
||||
const path = request.path ?? '';
|
||||
if (path === '/api/auth/session/sync') return result({ success: true, session: { accessToken: 'ui-fixture', tokenType: 'Bearer', expiresAt: Date.now() + 3600000, lastActiveAt: Date.now(), canRefresh: false } });
|
||||
if (path === '/api/auth/me') return result({ success: true, user: { username: 'creator', userId: 'creator', tenantId: null, deptId: null, authorities: [] }, moduleAccess: { programming: true, design: true, robot: true, cloud_agents: 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-13T00:00:00Z' } });
|
||||
if (!path.startsWith('/api/cloud-agents/')) return result({ success: true });
|
||||
const body = typeof request.body === 'string' ? JSON.parse(request.body) : request.body ?? {};
|
||||
if (path.endsWith('/recovery')) return result({ recent: null, pending: [] });
|
||||
if (path.endsWith('/recovery/recent')) return result({ saved: true });
|
||||
if (!path.endsWith('/actions')) return result(path.endsWith('/agents') || path.endsWith('/bootstrap') ? { agents: [draft, secondDraft], next_cursor: null } : path.endsWith(secondSlug) ? secondDraft : draft);
|
||||
const { operation, input = {} } = body;
|
||||
calls.push({ operation, input });
|
||||
if (operation === 'threads') return result({ threads: [], next_offset: null });
|
||||
if (operation === 'history') return result({ thread_id: 'desktop-thread', messages: [], run: null, next_offset: null });
|
||||
if (operation === 'attachments' || operation === 'files') return result({ attachments: [], files: [] });
|
||||
if (operation === 'catalog') return result({ models: [{ id: 'test-model', name: '创作模型' }], resources: {}, pricing: null });
|
||||
if (operation === 'access') return result({ published_version: 1, enabled: true, versions: [], grants: [], applications: [], share_url: 'niancode://agents/' + slug });
|
||||
if (operation === 'budget') return result({ request_limit_points: '2.50', daily_limit_points: '10.00', daily_committed_points: '0.20' });
|
||||
if (operation === 'costs') return result({ items: [], next_offset: null, summary: { count: 0, settled_points: '0', pending_points: '0' } });
|
||||
if (operation === 'channelAccounts') return result({ items: channelAccounts });
|
||||
if (operation === 'createChannelAccount') {
|
||||
const created = { ...spare, id: 'wechat-created', display_name: input.display_name, binding_id: 'binding-created' };
|
||||
channelAccounts.push(created);
|
||||
return result({ channel: created });
|
||||
ipcMain.handle(
|
||||
'hostapi:fetch',
|
||||
async (_event, request: { path?: string; method?: string; body?: string }) => {
|
||||
const path = request.path ?? '';
|
||||
if (path === '/api/auth/session/sync')
|
||||
return result({
|
||||
success: true,
|
||||
session: {
|
||||
accessToken: 'ui-fixture',
|
||||
tokenType: 'Bearer',
|
||||
expiresAt: Date.now() + 3600000,
|
||||
lastActiveAt: Date.now(),
|
||||
canRefresh: false,
|
||||
},
|
||||
});
|
||||
if (path === '/api/auth/me')
|
||||
return result({
|
||||
success: true,
|
||||
user: {
|
||||
username: 'creator',
|
||||
userId: 'creator',
|
||||
tenantId: null,
|
||||
deptId: null,
|
||||
authorities: [],
|
||||
},
|
||||
moduleAccess: { programming: true, design: true, robot: true, cloud_agents: 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-13T00:00:00Z',
|
||||
},
|
||||
});
|
||||
if (!path.startsWith('/api/cloud-agents/')) return result({ success: true });
|
||||
const body =
|
||||
typeof request.body === 'string' ? JSON.parse(request.body) : (request.body ?? {});
|
||||
if (path.endsWith('/recovery')) return result({ recent: null, pending: [] });
|
||||
if (path.endsWith('/recovery/recent')) return result({ saved: true });
|
||||
if (!path.endsWith('/actions'))
|
||||
return result(
|
||||
path.endsWith('/agents') || path.endsWith('/bootstrap')
|
||||
? { agents: [draft, secondDraft], next_cursor: null }
|
||||
: path.endsWith(secondSlug)
|
||||
? secondDraft
|
||||
: draft
|
||||
);
|
||||
const { operation, input = {} } = body;
|
||||
calls.push({ operation, input });
|
||||
if (operation === 'threads') return result({ threads: [], next_offset: null });
|
||||
if (operation === 'history')
|
||||
return result({ thread_id: 'desktop-thread', messages: [], run: null, next_offset: null });
|
||||
if (operation === 'attachments' || operation === 'files')
|
||||
return result({ attachments: [], files: [] });
|
||||
if (operation === 'catalog')
|
||||
return result({
|
||||
models: [{ id: 'test-model', name: '创作模型' }],
|
||||
resources: {},
|
||||
pricing: null,
|
||||
});
|
||||
if (operation === 'access')
|
||||
return result({
|
||||
published_version: 1,
|
||||
enabled: true,
|
||||
versions: [],
|
||||
grants: [],
|
||||
applications: [],
|
||||
share_url: 'niancode://agents/' + slug,
|
||||
});
|
||||
if (operation === 'budget')
|
||||
return result({
|
||||
request_limit_points: '2.50',
|
||||
daily_limit_points: '10.00',
|
||||
daily_committed_points: '0.20',
|
||||
});
|
||||
if (operation === 'costs')
|
||||
return result({
|
||||
items: [],
|
||||
next_offset: null,
|
||||
summary: { count: 0, settled_points: '0', pending_points: '0' },
|
||||
});
|
||||
if (operation === 'channelAccounts') return result({ items: channelAccounts });
|
||||
if (operation === 'createChannelAccount') {
|
||||
const created = {
|
||||
...spare,
|
||||
id: 'wechat-created',
|
||||
display_name: input.display_name,
|
||||
binding_id: 'binding-created',
|
||||
};
|
||||
channelAccounts.push(created);
|
||||
return result({ channel: created });
|
||||
}
|
||||
if (operation === 'routeChannelAccount') {
|
||||
const stale = { ...channelAccounts.find((item) => item.id === input.channel_account_id) };
|
||||
const target =
|
||||
input.target_agent_slug === secondSlug
|
||||
? secondDraft
|
||||
: input.target_agent_slug === slug
|
||||
? draft
|
||||
: null;
|
||||
channelAccounts = channelAccounts.map((item) =>
|
||||
item.id !== input.channel_account_id
|
||||
? item
|
||||
: {
|
||||
...item,
|
||||
revision: item.revision + 1,
|
||||
enabled: input.enabled,
|
||||
target_agent_slug: target?.slug ?? null,
|
||||
target_agent_name: target?.name ?? null,
|
||||
target_agent_published_version: target?.published_version ?? null,
|
||||
agent_slug: target?.slug ?? null,
|
||||
published_version: target?.published_version ?? null,
|
||||
target_agent_address: target ? 'yuxi:makelore:' + target.slug : null,
|
||||
blockers: target ? [] : ['target_required'],
|
||||
}
|
||||
);
|
||||
return result({ channel: stale, target_agent: target?.slug ?? null });
|
||||
}
|
||||
if (operation === 'enableChannelAccount' || operation === 'pauseChannelAccount') {
|
||||
const enabled = operation === 'enableChannelAccount';
|
||||
channelAccounts = channelAccounts.map((item) =>
|
||||
item.id !== input.channel_account_id
|
||||
? item
|
||||
: {
|
||||
...item,
|
||||
revision: item.revision + 1,
|
||||
enabled,
|
||||
desired_state: enabled ? 'enabled' : 'disabled',
|
||||
}
|
||||
);
|
||||
return result({
|
||||
channel: channelAccounts.find((item) => item.id === input.channel_account_id),
|
||||
});
|
||||
}
|
||||
if (operation === 'unbindChannelAccount') {
|
||||
if (rejectUnbind) {
|
||||
rejectUnbind = false;
|
||||
return {
|
||||
ok: true,
|
||||
data: {
|
||||
status: 409,
|
||||
ok: false,
|
||||
json: {
|
||||
error: '解除暂时失败,请重试',
|
||||
code: 'channel_operation_conflict',
|
||||
commandOutcome: 'definitive_failure',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
channelAccounts = channelAccounts.map((item) =>
|
||||
item.id === input.channel_account_id
|
||||
? {
|
||||
...item,
|
||||
health: 'login_required',
|
||||
enabled: false,
|
||||
desired_state: 'paused',
|
||||
sync_state: 'ready',
|
||||
target_agent_slug: null,
|
||||
target_agent_name: null,
|
||||
target_agent_address: '',
|
||||
target_agent_published_version: null,
|
||||
agent_slug: null,
|
||||
published_version: null,
|
||||
provider_generation: 'logged-out-generation',
|
||||
revision: item.revision + 1,
|
||||
}
|
||||
: item
|
||||
);
|
||||
return result({
|
||||
channel: channelAccounts.find((item) => item.id === input.channel_account_id),
|
||||
disconnected: true,
|
||||
unbound: true,
|
||||
});
|
||||
}
|
||||
if (operation === 'channelBindings')
|
||||
return result({
|
||||
items: channelAccounts.filter((item) => item.target_agent_slug === input.slug),
|
||||
available_accounts: [],
|
||||
});
|
||||
if (
|
||||
operation === 'channelAccountWechatBindStart' ||
|
||||
operation === 'channelAccountWechatBindStatus'
|
||||
)
|
||||
return result({
|
||||
session_key: 'qr-session',
|
||||
status: verified ? 'confirmed' : 'verification_required',
|
||||
qrcode_url: 'https://login.example.test/confirm?token=fixture',
|
||||
message: '请确认微信验证码',
|
||||
});
|
||||
if (operation === 'channelAccountWechatBindVerification') {
|
||||
verified = true;
|
||||
channelAccounts = channelAccounts.map((item) =>
|
||||
item.id === input.channel_account_id ? { ...item, health: 'connected' } : item
|
||||
);
|
||||
return result({ session_key: 'qr-session', status: 'confirmed', message: '连接已确认' });
|
||||
}
|
||||
if (operation === 'schedules') return result({ jobs: [] });
|
||||
if (operation === 'scheduleContext')
|
||||
return result({
|
||||
version: 1,
|
||||
enabled: true,
|
||||
configuration,
|
||||
result_destination: '任务历史',
|
||||
payer: 'creator',
|
||||
});
|
||||
throw new Error('Unexpected channel UI fixture operation ' + operation);
|
||||
}
|
||||
if (operation === 'routeChannelAccount') {
|
||||
const target = input.target_agent_slug === secondSlug ? secondDraft : input.target_agent_slug === slug ? draft : null;
|
||||
channelAccounts = channelAccounts.map(item => item.id !== input.channel_account_id ? item : { ...item, revision: item.revision + 1, enabled: input.enabled,
|
||||
target_agent_slug: target?.slug ?? null, target_agent_name: target?.name ?? null, target_agent_published_version: target?.published_version ?? null,
|
||||
agent_slug: target?.slug ?? null, published_version: target?.published_version ?? null, target_agent_address: target ? 'yuxi:makelore:' + target.slug : null,
|
||||
blockers: target ? [] : ['target_required'] });
|
||||
return result({ channel: channelAccounts.find(item => item.id === input.channel_account_id), target_agent: target?.slug ?? null });
|
||||
}
|
||||
if (operation === 'enableChannelAccount' || operation === 'pauseChannelAccount') {
|
||||
const enabled = operation === 'enableChannelAccount';
|
||||
channelAccounts = channelAccounts.map(item => item.id !== input.channel_account_id ? item : { ...item, revision: item.revision + 1, enabled, desired_state: enabled ? 'enabled' : 'disabled' });
|
||||
return result({ channel: channelAccounts.find(item => item.id === input.channel_account_id) });
|
||||
}
|
||||
if (operation === 'disconnectChannelAccount') {
|
||||
channelAccounts = channelAccounts.map(item => item.id === input.channel_account_id ? { ...item, health: 'login_required', enabled: false, revision: item.revision + 1 } : item);
|
||||
return result({ channel: channelAccounts.find(item => item.id === input.channel_account_id), disconnected: true });
|
||||
}
|
||||
if (operation === 'channelBindings') return result({ items: channelAccounts.filter(item => item.target_agent_slug === input.slug), available_accounts: [] });
|
||||
if (operation === 'channelAccountWechatBindStart' || operation === 'channelAccountWechatBindStatus') return result({ session_key: 'qr-session', status: verified ? 'confirmed' : 'verification_required', qrcode_url: 'https://login.example.test/confirm?token=fixture', message: '请确认微信验证码' });
|
||||
if (operation === 'channelAccountWechatBindVerification') { verified = true; channelAccounts = channelAccounts.map(item => item.id === input.channel_account_id ? { ...item, health: 'connected' } : item); return result({ session_key: 'qr-session', status: 'confirmed', message: '连接已确认' }); }
|
||||
if (operation === 'schedules') return result({ jobs: [] });
|
||||
if (operation === 'scheduleContext') return result({ version: 1, enabled: true, configuration, result_destination: '任务历史', payer: 'creator' });
|
||||
throw new Error('Unexpected channel UI fixture operation ' + operation);
|
||||
});
|
||||
);
|
||||
});
|
||||
await page.goto(applicationUrl);
|
||||
await page.getByTestId('ai-module-option-cloud_agents').click();
|
||||
await page.getByRole('article', { name: '我的创作助手', exact: true }).getByRole('button', { name: '和它聊聊', exact: true }).click();
|
||||
await page
|
||||
.getByRole('article', { name: '我的创作助手', exact: true })
|
||||
.getByRole('button', { name: '和它聊聊', exact: true })
|
||||
.click();
|
||||
await expect(page.getByRole('region', { name: '智能体对话', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: '我的创作助手', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: '微信对话', exact: true })).toHaveCount(0);
|
||||
@@ -151,7 +479,7 @@ test('WeChat binding cards connect an account and switch its Agent through the c
|
||||
const legacyCard = page.getByRole('article', { name: '创作微信微信', exact: true });
|
||||
const spareCard = page.getByRole('article', { name: '备用微信微信', exact: true });
|
||||
await expect(legacyCard.getByText('我的创作助手', { exact: true })).toBeVisible();
|
||||
await expect(spareCard.getByRole('button', { name: '继续绑定', exact: true })).toBeVisible();
|
||||
await expect(spareCard.getByRole('button', { name: '绑定智能体', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('dialog')).toHaveCount(0);
|
||||
await expect(page.getByText('活动记录', { exact: true })).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: '微信对话', exact: true })).toHaveCount(0);
|
||||
@@ -159,59 +487,225 @@ test('WeChat binding cards connect an account and switch its Agent through the c
|
||||
|
||||
await page.getByRole('button', { name: '添加微信', exact: true }).click();
|
||||
const adding = page.getByRole('dialog', { name: '添加微信', exact: true });
|
||||
await adding.getByLabel('选择智能体', { exact: true }).selectOption(agentSlug);
|
||||
await adding.getByLabel('微信备注(可选)', { exact: true }).fill('新创作微信');
|
||||
await adding
|
||||
.getByRole('radiogroup', { name: '选择智能体' })
|
||||
.getByText('我的创作助手', { exact: true })
|
||||
.click();
|
||||
await expect(adding.getByRole('radio', { name: '我的创作助手', exact: true })).toBeChecked();
|
||||
await adding.getByRole('button', { name: '下一步:扫码连接', exact: true }).click();
|
||||
await expect(adding.getByRole('img', { name: '个人微信登录二维码', exact: true })).toHaveAttribute('src', /^data:image\//);
|
||||
await expect.poll(async () => app.evaluate(() => (globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string; input: Record<string, unknown> }[] }).cloudChannelFixtureCalls
|
||||
.filter(call => call.operation === 'routeChannelAccount').some(call => call.input.channel_account_id === 'wechat-created' && call.input.target_agent_slug === 'ml-' + 'a'.repeat(32) && call.input.enabled === false))).toBe(true);
|
||||
await expect(adding.getByRole('button', { name: '完成绑定并启用', exact: true })).toBeDisabled();
|
||||
await expect(adding.getByRole('img', { name: '个人微信登录二维码', exact: true })).toHaveAttribute(
|
||||
'src',
|
||||
/^data:image\//
|
||||
);
|
||||
await expect
|
||||
.poll(async () =>
|
||||
app.evaluate(() =>
|
||||
(
|
||||
globalThis as typeof globalThis & {
|
||||
cloudChannelFixtureCalls: { operation: string; input: Record<string, unknown> }[];
|
||||
}
|
||||
).cloudChannelFixtureCalls
|
||||
.filter((call) => call.operation === 'routeChannelAccount')
|
||||
.some(
|
||||
(call) =>
|
||||
call.input.channel_account_id === 'wechat-created' &&
|
||||
call.input.target_agent_slug === 'ml-' + 'a'.repeat(32) &&
|
||||
call.input.enabled === false
|
||||
)
|
||||
)
|
||||
)
|
||||
.toBe(true);
|
||||
await expect(adding.getByRole('button', { name: '完成绑定并启用', exact: true })).toHaveCount(0);
|
||||
await adding.getByLabel('微信验证码', { exact: true }).fill('123456');
|
||||
await adding.getByRole('button', { name: '提交验证码', exact: true }).click();
|
||||
await expect(adding.getByRole('img', { name: '个人微信登录二维码', exact: true })).toHaveCount(0);
|
||||
await expect(adding.getByLabel('微信验证码', { exact: true })).toHaveCount(0);
|
||||
await expect(adding.getByText('微信已连接', { exact: true })).toBeVisible();
|
||||
expect(await app.evaluate(() => (globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string }[] }).cloudChannelFixtureCalls.some(call => call.operation === 'enableChannelAccount'))).toBe(false);
|
||||
await page.screenshot({ path: testInfo.outputPath('wechat-connection-confirmed.png'), fullPage: true });
|
||||
await adding.getByRole('button', { name: '完成绑定并启用', exact: true }).click();
|
||||
await expect(adding.getByRole('heading', { name: '微信绑定成功', exact: true })).toBeVisible();
|
||||
expect(
|
||||
await app.evaluate(
|
||||
() =>
|
||||
(
|
||||
globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string }[] }
|
||||
).cloudChannelFixtureCalls.filter((call) => call.operation === 'enableChannelAccount').length
|
||||
)
|
||||
).toBe(1);
|
||||
await page.screenshot({
|
||||
path: testInfo.outputPath('wechat-automatically-connected.png'),
|
||||
fullPage: true,
|
||||
});
|
||||
await expect(adding.getByRole('button', { name: '完成', exact: true })).toBeVisible();
|
||||
await adding.getByRole('button', { name: '完成', exact: true }).click();
|
||||
await expect(page.getByRole('article', { name: '新创作微信微信', exact: true }).getByText('我的创作助手', { exact: true })).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByRole('article', { name: '微信 3微信', exact: true })
|
||||
.getByText('我的创作助手', { exact: true })
|
||||
).toBeVisible();
|
||||
|
||||
await legacyCard.getByRole('button', { name: '管理绑定', exact: true }).click();
|
||||
const managing = page.getByRole('dialog', { name: '管理微信绑定', exact: true });
|
||||
const qrStartsBeforeSwitch = await app.evaluate(() => (globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string }[] }).cloudChannelFixtureCalls.filter(call => call.operation === 'channelAccountWechatBindStart').length);
|
||||
await managing.getByLabel('绑定的智能体', { exact: true }).selectOption(secondAgentSlug);
|
||||
await managing.getByRole('button', { name: '保存更换', exact: true }).click();
|
||||
const switching = page.getByRole('dialog', { name: '更换绑定的智能体', exact: true });
|
||||
await switching.getByRole('button', { name: /确认更换/ }).click();
|
||||
await expect.poll(async () => app.evaluate(() => (globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string; input: Record<string, unknown> }[] }).cloudChannelFixtureCalls
|
||||
.filter(call => call.operation === 'routeChannelAccount').some(call => call.input.channel_account_id === 'wechat-a' && call.input.target_agent_slug === 'ml-' + 'b'.repeat(32) && call.input.enabled === true))).toBe(true);
|
||||
await expect(managing.getByText('已更换绑定的智能体,微信连接保留。', { exact: true })).toBeVisible();
|
||||
await expect(managing.getByLabel('绑定的智能体', { exact: true })).toHaveValue(secondAgentSlug);
|
||||
expect(await app.evaluate(() => (globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string }[] }).cloudChannelFixtureCalls.filter(call => call.operation === 'channelAccountWechatBindStart').length)).toBe(qrStartsBeforeSwitch);
|
||||
await legacyCard.getByRole('button', { name: '更换智能体', exact: true }).click();
|
||||
const managing = page.getByRole('dialog', { name: '更换智能体', exact: true });
|
||||
const qrStartsBeforeSwitch = await app.evaluate(
|
||||
() =>
|
||||
(
|
||||
globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string }[] }
|
||||
).cloudChannelFixtureCalls.filter((call) => call.operation === 'channelAccountWechatBindStart')
|
||||
.length
|
||||
);
|
||||
await managing
|
||||
.getByRole('radiogroup', { name: '选择智能体' })
|
||||
.getByText('资料整理助手', { exact: true })
|
||||
.click();
|
||||
await expect(managing.getByRole('radio', { name: '资料整理助手', exact: true })).toBeChecked();
|
||||
await expect(page.getByRole('dialog')).toHaveCount(1);
|
||||
await page.screenshot({ path: testInfo.outputPath('wechat-agent-choice.png'), fullPage: true });
|
||||
await managing.getByRole('button', { name: '确认更换', exact: true }).click();
|
||||
await expect
|
||||
.poll(async () =>
|
||||
app.evaluate(() =>
|
||||
(
|
||||
globalThis as typeof globalThis & {
|
||||
cloudChannelFixtureCalls: { operation: string; input: Record<string, unknown> }[];
|
||||
}
|
||||
).cloudChannelFixtureCalls
|
||||
.filter((call) => call.operation === 'routeChannelAccount')
|
||||
.some(
|
||||
(call) =>
|
||||
call.input.channel_account_id === 'wechat-a' &&
|
||||
call.input.target_agent_slug === 'ml-' + 'b'.repeat(32) &&
|
||||
call.input.enabled === true
|
||||
)
|
||||
)
|
||||
)
|
||||
.toBe(true);
|
||||
await expect(managing).toHaveCount(0);
|
||||
await expect(page.getByText('已更换智能体,微信连接和历史保留。', { exact: true })).toBeVisible();
|
||||
expect(
|
||||
await app.evaluate(
|
||||
() =>
|
||||
(
|
||||
globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string }[] }
|
||||
).cloudChannelFixtureCalls.filter((call) => call.operation === 'channelAccountWechatBindStart')
|
||||
.length
|
||||
)
|
||||
).toBe(qrStartsBeforeSwitch);
|
||||
await expect(page.getByText('活动记录', { exact: true })).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: '微信对话', exact: true })).toHaveCount(0);
|
||||
await expect(page.getByText(/本人|受邀|邀请|使用范围|使用权限|授权调用者/)).toHaveCount(0);
|
||||
await app.evaluate(({ BrowserWindow }) => { const win = BrowserWindow.getAllWindows()[0]; win.setMinimumSize(800, 600); win.setSize(980, 680); });
|
||||
await expect.poll(() => page.getByTestId('main-content').evaluate(element => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(1);
|
||||
await page.screenshot({ path: testInfo.outputPath('wechat-manage-binding-compact.png'), fullPage: true });
|
||||
await managing.getByRole('button', { name: '关闭', exact: true }).click();
|
||||
await expect(managing).toHaveCount(0);
|
||||
await expect(legacyCard.getByText('资料整理助手', { exact: true })).toBeVisible();
|
||||
const finalCalls = await app.evaluate(() => (globalThis as typeof globalThis & { cloudChannelFixtureCalls: { operation: string; input: Record<string, unknown> }[] }).cloudChannelFixtureCalls);
|
||||
expect(finalCalls.some(call => ['channelActivity', 'channelConversations', 'channelConversation', 'channelConversationControl', 'channelFiles', 'downloadChannelArtifact', 'channelPolicy', 'createChannelPairing', 'channelCallers', 'channelSelfCallers', 'revokeChannelCaller', 'submit', 'preview', 'resume'].includes(call.operation))).toBe(false);
|
||||
expect(finalCalls.filter(call => call.operation === 'createChannelAccount')).toHaveLength(1);
|
||||
expect(finalCalls.find(call => call.operation === 'createChannelAccount')?.input).toEqual({ display_name: '新创作微信', operation_id: expect.any(String) });
|
||||
expect(finalCalls.find(call => call.operation === 'channelAccountWechatBindVerification')?.input).toEqual({ channel_account_id: 'wechat-created', session_key: 'qr-session', verify_code: '123456', operation_id: expect.any(String) });
|
||||
expect(finalCalls.find(call => call.operation === 'enableChannelAccount')?.input).toEqual({ channel_account_id: 'wechat-created', expected_revision: 2, operation_id: expect.any(String) });
|
||||
expect(finalCalls.filter(call => call.operation === 'routeChannelAccount').map(call => call.input)).toEqual([
|
||||
{ channel_account_id: 'wechat-created', target_agent_slug: agentSlug, expected_revision: 1, enabled: false, operation_id: expect.any(String) },
|
||||
{ channel_account_id: 'wechat-a', target_agent_slug: secondAgentSlug, expected_revision: 3, enabled: true, operation_id: expect.any(String) },
|
||||
]);
|
||||
await app.evaluate(({ BrowserWindow }) => {
|
||||
const win = BrowserWindow.getAllWindows()[0];
|
||||
win.setMinimumSize(800, 600);
|
||||
win.setSize(980, 680);
|
||||
});
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.getByTestId('main-content').evaluate((element) => element.scrollWidth - element.clientWidth)
|
||||
)
|
||||
.toBeLessThanOrEqual(1);
|
||||
await page.screenshot({
|
||||
path: testInfo.outputPath('wechat-binding-cards-compact.png'),
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
await expect(legacyCard.getByText('资料整理助手', { exact: true })).toBeVisible();
|
||||
const finalCalls = await app.evaluate(
|
||||
() =>
|
||||
(
|
||||
globalThis as typeof globalThis & {
|
||||
cloudChannelFixtureCalls: { operation: string; input: Record<string, unknown> }[];
|
||||
}
|
||||
).cloudChannelFixtureCalls
|
||||
);
|
||||
expect(
|
||||
finalCalls.some((call) =>
|
||||
[
|
||||
'channelActivity',
|
||||
'channelConversations',
|
||||
'channelConversation',
|
||||
'channelConversationControl',
|
||||
'channelFiles',
|
||||
'downloadChannelArtifact',
|
||||
'channelPolicy',
|
||||
'createChannelPairing',
|
||||
'channelCallers',
|
||||
'channelSelfCallers',
|
||||
'revokeChannelCaller',
|
||||
'submit',
|
||||
'preview',
|
||||
'resume',
|
||||
].includes(call.operation)
|
||||
)
|
||||
).toBe(false);
|
||||
expect(finalCalls.filter((call) => call.operation === 'createChannelAccount')).toHaveLength(1);
|
||||
expect(finalCalls.find((call) => call.operation === 'createChannelAccount')?.input).toEqual({
|
||||
display_name: '微信 3',
|
||||
operation_id: expect.any(String),
|
||||
});
|
||||
expect(
|
||||
finalCalls.find((call) => call.operation === 'channelAccountWechatBindVerification')?.input
|
||||
).toEqual({
|
||||
channel_account_id: 'wechat-created',
|
||||
session_key: 'qr-session',
|
||||
verify_code: '123456',
|
||||
operation_id: expect.any(String),
|
||||
});
|
||||
expect(finalCalls.find((call) => call.operation === 'enableChannelAccount')?.input).toEqual({
|
||||
channel_account_id: 'wechat-created',
|
||||
expected_revision: 2,
|
||||
operation_id: expect.any(String),
|
||||
});
|
||||
expect(
|
||||
finalCalls.filter((call) => call.operation === 'routeChannelAccount').map((call) => call.input)
|
||||
).toEqual([
|
||||
{
|
||||
channel_account_id: 'wechat-created',
|
||||
target_agent_slug: agentSlug,
|
||||
expected_revision: 1,
|
||||
enabled: false,
|
||||
operation_id: expect.any(String),
|
||||
},
|
||||
{
|
||||
channel_account_id: 'wechat-a',
|
||||
target_agent_slug: secondAgentSlug,
|
||||
expected_revision: 3,
|
||||
enabled: true,
|
||||
operation_id: expect.any(String),
|
||||
},
|
||||
]);
|
||||
await legacyCard.getByRole('button', { name: '更多操作', exact: true }).click();
|
||||
await page.screenshot({ path: testInfo.outputPath('wechat-binding-more-compact.png'), fullPage: true });
|
||||
await expect(legacyCard.getByRole('button', { name: '暂停自动回复', exact: true })).toBeVisible();
|
||||
await legacyCard.getByRole('button', { name: '解除微信绑定', exact: true }).click();
|
||||
const unbinding = page.getByRole('dialog', { name: '解除微信绑定', exact: true });
|
||||
await unbinding.getByRole('button', { name: '确认解除微信绑定', exact: true }).click();
|
||||
await expect(unbinding.getByRole('alert')).toHaveText(/解除暂时失败/);
|
||||
await expect(page.getByRole('dialog')).toHaveCount(1);
|
||||
await page.screenshot({
|
||||
path: testInfo.outputPath('wechat-unbind-visible-failure.png'),
|
||||
fullPage: true,
|
||||
});
|
||||
await unbinding.getByRole('button', { name: '确认解除微信绑定', exact: true }).click();
|
||||
await expect(unbinding).toHaveCount(0);
|
||||
await expect(legacyCard.getByText('未绑定', { exact: true })).toBeVisible();
|
||||
await expect(legacyCard.getByRole('button', { name: '绑定智能体', exact: true })).toBeVisible();
|
||||
await expect(page.getByText('断开微信连接', { exact: true })).toHaveCount(0);
|
||||
await page.screenshot({
|
||||
path: testInfo.outputPath('wechat-unbound-card-compact.png'),
|
||||
fullPage: true,
|
||||
});
|
||||
const unbindCalls = await app.evaluate(() =>
|
||||
(
|
||||
globalThis as typeof globalThis & {
|
||||
cloudChannelFixtureCalls: { operation: string; input: Record<string, unknown> }[];
|
||||
}
|
||||
).cloudChannelFixtureCalls.filter((call) => call.operation === 'unbindChannelAccount')
|
||||
);
|
||||
expect(unbindCalls).toHaveLength(2);
|
||||
expect(unbindCalls[0].input).toEqual(unbindCalls[1].input);
|
||||
} finally {
|
||||
await app.evaluate(({ BrowserWindow }) => { for (const window of BrowserWindow.getAllWindows()) window.destroy(); }).catch(() => undefined);
|
||||
await app
|
||||
.evaluate(({ BrowserWindow }) => {
|
||||
for (const window of BrowserWindow.getAllWindows()) window.destroy();
|
||||
})
|
||||
.catch(() => undefined);
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
@@ -221,8 +221,14 @@ async function expectComposerBounds(page: Page) {
|
||||
.map(element => ({ width: element.scrollWidth - element.clientWidth, name: element.getAttribute('data-testid') ?? element.className })),
|
||||
composer: (() => {
|
||||
const bounds = document.querySelector('[data-testid="cloud-chat-composer"]')!.getBoundingClientRect();
|
||||
return { left: bounds.left, right: bounds.right, bottom: bounds.bottom };
|
||||
return { left: bounds.left, right: bounds.right, bottom: bounds.bottom, width: bounds.width };
|
||||
})(),
|
||||
controls: [...document.querySelectorAll('[data-testid="cloud-chat-composer"] :is(.agent-attachment-add, .agent-chat-submit, [aria-label="语音输入"])')]
|
||||
.map(element => {
|
||||
const bounds = element.getBoundingClientRect();
|
||||
const style = getComputedStyle(element);
|
||||
return { width: bounds.width, height: bounds.height, radius: Number.parseFloat(style.borderTopLeftRadius) };
|
||||
}),
|
||||
}));
|
||||
expect(overflow.document).toBeLessThanOrEqual(1);
|
||||
expect(overflow.body).toBeLessThanOrEqual(1);
|
||||
@@ -230,6 +236,13 @@ async function expectComposerBounds(page: Page) {
|
||||
expect(overflow.composer.left).toBeGreaterThanOrEqual(0);
|
||||
expect(overflow.composer.right).toBeLessThanOrEqual(page.viewportSize()!.width);
|
||||
expect(overflow.composer.bottom).toBeLessThanOrEqual(page.viewportSize()!.height);
|
||||
expect(overflow.composer.width).toBeLessThanOrEqual(800);
|
||||
expect(overflow.controls).toHaveLength(3);
|
||||
for (const control of overflow.controls) {
|
||||
expect(control.width).toBe(32);
|
||||
expect(control.height).toBe(32);
|
||||
expect(control.radius).toBeGreaterThanOrEqual(16);
|
||||
}
|
||||
}
|
||||
async function operations(page: Page) {
|
||||
return page.evaluate(() => (window as unknown as FixtureWindow).__cloudChatFixture.calls
|
||||
@@ -316,12 +329,21 @@ for (const viewport of [{ width: 375, height: 540 }, { width: 768, height: 760 }
|
||||
const composer = page.getByTestId('cloud-chat-composer');
|
||||
await expect(transcript.getByRole('article')).toHaveCount(16);
|
||||
const userBubble = transcript.locator('.agent-chat-message[data-role="user"]').first();
|
||||
const bubbleStyle = await userBubble.evaluate(element => {
|
||||
const bubbleBody = userBubble.locator('.agent-chat-message-body');
|
||||
const bubbleStyle = await bubbleBody.evaluate(element => {
|
||||
const style = getComputedStyle(element);
|
||||
return { background: style.backgroundColor, radius: Number.parseFloat(style.borderTopLeftRadius) };
|
||||
});
|
||||
expect(bubbleStyle.background).not.toBe('rgba(0, 0, 0, 0)');
|
||||
expect(bubbleStyle.radius).toBeGreaterThan(0);
|
||||
await expect(userBubble).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
|
||||
const sentTime = userBubble.locator('time');
|
||||
await expect(sentTime).toHaveAttribute('aria-label', /^发送时间:/);
|
||||
const bodyBounds = await bubbleBody.boundingBox();
|
||||
const timeBounds = await sentTime.boundingBox();
|
||||
expect(bodyBounds).not.toBeNull();
|
||||
expect(timeBounds).not.toBeNull();
|
||||
expect(timeBounds!.y).toBeGreaterThanOrEqual(bodyBounds!.y + bodyBounds!.height + 6);
|
||||
await expect(transcript.locator('pre')).toHaveCount(1);
|
||||
await expect(transcript.locator('table')).toHaveCount(1);
|
||||
await expectComposerBounds(page);
|
||||
@@ -336,7 +358,10 @@ for (const viewport of [{ width: 375, height: 540 }, { width: 768, height: 760 }
|
||||
await transcript.evaluate(element => { element.scrollTop = element.scrollHeight; });
|
||||
await screenshot(page, `agent-chat-long-history-${viewport.width}`);
|
||||
const input = page.getByLabel('消息', { exact: true });
|
||||
const idleInputHeight = (await input.boundingBox())!.height;
|
||||
await input.fill(Array.from({ length: 6 }, (_, index) => `第 ${index + 1} 行未发送的故事想法`).join('\n'));
|
||||
await expect(input).toHaveAttribute('rows', '6');
|
||||
expect((await input.boundingBox())!.height).toBeGreaterThan(idleInputHeight);
|
||||
await expectComposerBounds(page);
|
||||
await expect(page.getByRole('button', { name: '查看历史对话', exact: true })).toHaveCount(0);
|
||||
await expect(page.getByLabel('历史对话', { exact: true })).toHaveCount(0);
|
||||
@@ -408,7 +433,7 @@ for (const width of [375, 1440]) {
|
||||
|
||||
test('running conversation keeps Stop reachable on a short narrow screen', async ({ page }) => {
|
||||
const errors = await openChat(page, 'running', 375, 540);
|
||||
const stop = page.getByRole('button', { name: '停止回答', exact: true });
|
||||
const stop = page.getByRole('button', { name: '停止', exact: true });
|
||||
await expect(stop).toBeInViewport({ ratio: 1 });
|
||||
await expectComposerBounds(page);
|
||||
await screenshot(page, 'agent-chat-running-375');
|
||||
@@ -472,7 +497,7 @@ for (const viewport of [{ width: 375, height: 540 }, { width: 1440, height: 900
|
||||
}, { reasoning, answer, date });
|
||||
// Native terminal events can reuse the last message ID and still finish a run.
|
||||
await emitRunEvent(page, 'end', '1003-0', { ...envelope, payload: { chunk: { status: 'completed' } } });
|
||||
await expect(page.getByRole('button', { name: '停止回答', exact: true })).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: '停止', exact: true })).toHaveCount(0);
|
||||
await expect(thinking).toHaveCount(1);
|
||||
await expect(thinking).not.toHaveAttribute('open');
|
||||
await expect(transcript.getByRole('article')).toHaveCount(1);
|
||||
|
||||
@@ -203,12 +203,24 @@ async function expectKnowledgeLayout(page: Page) {
|
||||
const ownedBounds = await knowledge.locator('.agent-knowledge-owned-workspace').boundingBox();
|
||||
expect(creationBounds).not.toBeNull(); expect(managementBounds).not.toBeNull(); expect(selectionBounds).not.toBeNull();
|
||||
expect(ownedBounds).not.toBeNull();
|
||||
if (ownedBounds!.width > 620) {
|
||||
if (ownedBounds!.width > 840) {
|
||||
expect(creationBounds!.x + creationBounds!.width).toBeLessThanOrEqual(managementBounds!.x + 1);
|
||||
expect(Math.abs(creationBounds!.y - managementBounds!.y)).toBeLessThanOrEqual(1);
|
||||
expect(Math.abs(creationBounds!.height - managementBounds!.height)).toBeLessThanOrEqual(1);
|
||||
expect(selectionBounds!.y).toBeGreaterThanOrEqual(Math.max(creationBounds!.y + creationBounds!.height, managementBounds!.y + managementBounds!.height));
|
||||
expect(Math.abs(selectionBounds!.x + selectionBounds!.width - managementBounds!.x - managementBounds!.width)).toBeLessThanOrEqual(1);
|
||||
const create = await creation.getByRole('button', { name: '新建知识库', exact: true }).boundingBox();
|
||||
const manager = await management.getByRole('combobox', { name: '管理的知识库', exact: true }).boundingBox();
|
||||
const refresh = await management.getByRole('button', { name: '刷新知识库与模型', exact: true }).boundingBox();
|
||||
const upload = await management.getByRole('button', { name: '从电脑上传', exact: true }).boundingBox();
|
||||
const controls = await management.locator('.agent-knowledge-management-controls').boundingBox();
|
||||
for (const control of [manager, refresh, upload]) {
|
||||
expect(Math.abs(control!.y + control!.height / 2 - create!.y - create!.height / 2)).toBeLessThanOrEqual(1);
|
||||
}
|
||||
expect(Math.abs(manager!.width - (controls!.width - 144 - 8) / 2)).toBeLessThanOrEqual(2);
|
||||
if (await creation.getByRole('textbox').count() === 0 && await management.getByRole('combobox', { name: '管理的知识库', exact: true }).inputValue() === '') {
|
||||
expect(Math.abs(create!.y + create!.height / 2 - creationBounds!.y - creationBounds!.height / 2)).toBeLessThanOrEqual(2);
|
||||
}
|
||||
} else {
|
||||
expect(creationBounds!.y + creationBounds!.height).toBeLessThanOrEqual(managementBounds!.y + 1);
|
||||
expect(managementBounds!.y + managementBounds!.height).toBeLessThanOrEqual(selectionBounds!.y + 1);
|
||||
@@ -217,6 +229,20 @@ async function expectKnowledgeLayout(page: Page) {
|
||||
expect(Math.abs(creationBounds!.width - selectionBounds!.width)).toBeLessThanOrEqual(1);
|
||||
}
|
||||
expect(Math.abs(creationBounds!.x - selectionBounds!.x)).toBeLessThanOrEqual(1);
|
||||
const choice = await selection.getByRole('combobox', { name: '选择知识库', exact: true }).boundingBox();
|
||||
const choiceRow = await selection.locator('.agent-knowledge-selection-controls').boundingBox();
|
||||
if (ownedBounds!.width > 840) {
|
||||
expect(Math.abs(choice!.width - (choiceRow!.width - 102) * 2 / 3)).toBeLessThanOrEqual(2);
|
||||
}
|
||||
const selectedList = selection.getByRole('list', { name: '已选知识库', exact: true });
|
||||
if (await selectedList.count()) {
|
||||
const selectedBounds = await selectedList.boundingBox();
|
||||
expect(Math.abs(selectedBounds!.x - choice!.x)).toBeLessThanOrEqual(1);
|
||||
expect(Math.abs(selectedBounds!.width - choice!.width)).toBeLessThanOrEqual(1);
|
||||
}
|
||||
for (const refresh of [management.getByRole('button', { name: '刷新知识库与模型', exact: true }), selection.getByRole('button', { name: '刷新知识库', exact: true })]) {
|
||||
expect(await refresh.evaluate(element => getComputedStyle(element).borderTopWidth)).toBe('1px');
|
||||
}
|
||||
await expectNoClipping(page);
|
||||
}
|
||||
async function screenshotKnowledge(page: Page, name: string) {
|
||||
@@ -356,6 +382,16 @@ test('cancelled and failed uploads preserve shared choices and the personal draf
|
||||
// An uncertain upload response keeps its retry attached to the same library.
|
||||
await expect(knowledge.getByRole('combobox', { name: '管理的知识库', exact: true })).toBeDisabled();
|
||||
await expect(knowledge.getByRole('button', { name: '新建知识库', exact: true })).toBeDisabled();
|
||||
await page.setViewportSize({ width: 1440, height: 850 });
|
||||
const managementCard = knowledge.getByRole('region', { name: '查看我的知识库', exact: true });
|
||||
const managementBounds = await managementCard.boundingBox();
|
||||
for (const control of await managementCard.locator('.agent-knowledge-management-controls').getByRole('button').all()) {
|
||||
const bounds = await control.boundingBox();
|
||||
expect(bounds!.x).toBeGreaterThanOrEqual(managementBounds!.x);
|
||||
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(managementBounds!.x + managementBounds!.width);
|
||||
}
|
||||
await expectNoClipping(page);
|
||||
await page.setViewportSize({ width: 375, height: 850 });
|
||||
const failedUpload = (await calls(page)).filter(call => call.path.endsWith('/knowledge/pick')).at(-1)!;
|
||||
await page.evaluate(() => { (window as unknown as FixtureWindow).__knowledgeFixture.upload = 'cancel'; });
|
||||
await knowledge.getByRole('button', { name: '重新选择同一文件并重试', exact: true }).click();
|
||||
|
||||
@@ -12,6 +12,36 @@ test('personal cloud Agent identity, configuration and separate published chat i
|
||||
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||
const applicationUrl = page.url();
|
||||
await page.goto('about:blank');
|
||||
// Exercise the shared voice path without requesting a real microphone.
|
||||
await page.addInitScript(() => {
|
||||
class FixtureMediaRecorder {
|
||||
static isTypeSupported() { return true; }
|
||||
state = 'inactive';
|
||||
mimeType = 'audio/webm';
|
||||
ondataavailable: ((event: { data: Blob }) => void) | null = null;
|
||||
onstop: (() => void) | null = null;
|
||||
start() { this.state = 'recording'; }
|
||||
stop() {
|
||||
this.state = 'inactive';
|
||||
this.ondataavailable?.({ data: new Blob(['fixture voice'], { type: 'audio/webm' }) });
|
||||
this.onstop?.();
|
||||
}
|
||||
}
|
||||
class FixtureAudioContext {
|
||||
async decodeAudioData() { return { duration: 0.1 }; }
|
||||
async close() { /* The fixture does not open an audio device. */ }
|
||||
}
|
||||
class FixtureOfflineAudioContext {
|
||||
destination = {};
|
||||
createBufferSource() { return { buffer: null, connect() {}, start() {} }; }
|
||||
async startRendering() { return { getChannelData: () => new Float32Array(1600) }; }
|
||||
}
|
||||
Object.defineProperty(navigator, 'mediaDevices', { configurable: true,
|
||||
value: { getUserMedia: async () => ({ getTracks: () => [{ stop() {} }] }) } });
|
||||
Object.defineProperty(window, 'MediaRecorder', { configurable: true, value: FixtureMediaRecorder });
|
||||
Object.defineProperty(window, 'AudioContext', { configurable: true, value: FixtureAudioContext });
|
||||
Object.defineProperty(window, 'OfflineAudioContext', { configurable: true, value: FixtureOfflineAudioContext });
|
||||
});
|
||||
// This UI test owns the Host API boundary; real cloud HTTP/PG is tested separately.
|
||||
await app.evaluate(({ ipcMain, BrowserWindow }, uploadedAvatar) => {
|
||||
BrowserWindow.getAllWindows()[0].setSize(1366, 768);
|
||||
@@ -21,7 +51,8 @@ test('personal cloud Agent identity, configuration and separate published chat i
|
||||
const creationInputs: Record<string, unknown>[] = [];
|
||||
let pending: Record<string, unknown>[] = [];
|
||||
const operations: string[] = [];
|
||||
Reflect.set(globalThis, '__cloudAgentUiFixture', { snapshot: () => ({ draft, createInput, creationInputs, operations }) });
|
||||
let speechRequests = 0;
|
||||
Reflect.set(globalThis, '__cloudAgentUiFixture', { snapshot: () => ({ draft, createInput, creationInputs, operations, speechRequests }) });
|
||||
const versions: { version: number; draft_revision: number; created_at: string }[] = [];
|
||||
let knowledgeCatalogLoads = 0;
|
||||
let knowledgeCreated = false;
|
||||
@@ -46,6 +77,16 @@ test('personal cloud Agent identity, configuration and separate published chat i
|
||||
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-10T00:00:00Z' },
|
||||
});
|
||||
if (path === '/api/works/speech/transcriptions') {
|
||||
const speech = JSON.parse(request.body ?? '{}');
|
||||
if (speech.mimeType !== 'audio/wav' || speech.fileName !== 'voice.wav' || speech.language !== 'zh'
|
||||
|| typeof speech.audioBase64 !== 'string' || !speech.audioBase64.startsWith('UklGR')) {
|
||||
throw new Error('Unexpected speech transcription fixture payload');
|
||||
}
|
||||
speechRequests++;
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
return result({ success: true, transcription: { text: '加上语音描述', model: 'fixture-transcribe' } });
|
||||
}
|
||||
if (path.startsWith('/api/cloud-agents/')) {
|
||||
const body = typeof request.body === 'string' ? JSON.parse(request.body) : request.body ?? {};
|
||||
if (path.endsWith('/avatars/pick')) return result(uploadedAvatar);
|
||||
@@ -189,9 +230,9 @@ test('personal cloud Agent identity, configuration and separate published chat i
|
||||
await page.getByRole('button', { name: '下一步', exact: true }).click();
|
||||
await expect(page.getByLabel('名称', { exact: true })).toBeDisabled();
|
||||
await expect(page.getByRole('button', { name: '更换头像', exact: true })).toBeDisabled();
|
||||
await expect(page.getByRole('complementary', { name: '未确认的云端操作' })).toBeVisible();
|
||||
await page.getByRole('button', { name: '移除提醒', exact: true }).click();
|
||||
await expect(page.getByRole('complementary', { name: '未确认的云端操作' })).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: '返回列表确认', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('dialog', { name: '创建记录', exact: true })).toHaveCount(0);
|
||||
await page.getByRole('button', { name: '重试创建', exact: true }).click();
|
||||
await expect(page.getByRole('alert')).toContainText('创建结果暂未确认');
|
||||
await expect(page.getByLabel('名称', { exact: true })).toBeDisabled();
|
||||
@@ -306,6 +347,41 @@ test('personal cloud Agent identity, configuration and separate published chat i
|
||||
expect(await messageOperations()).toEqual([]);
|
||||
await expect(formalChat.getByRole('heading', { name: 'Hi,今天想聊些什么?', exact: true })).toBeVisible();
|
||||
await expect(formalChat.locator('.agent-chat-welcome').getByRole('img', { name: '我的写作搭档头像', exact: true })).toHaveAttribute('src', avatar.icon_url);
|
||||
const voiceComposer = formalChat.getByTestId('cloud-chat-composer');
|
||||
const voiceDraft = formalChat.getByLabel('消息', { exact: true });
|
||||
await expect(voiceComposer.locator('.agent-attachment-composer')).toHaveCount(1);
|
||||
await expect(voiceComposer.getByRole('button', { name: '语音输入', exact: true })).toHaveClass(/rounded-full/);
|
||||
await expect(voiceDraft).toHaveAttribute('rows', '1');
|
||||
const sendGeometry = await voiceComposer.getByRole('button', { name: '发送', exact: true }).evaluate(element => {
|
||||
const style = getComputedStyle(element);
|
||||
return { width: element.clientWidth, height: element.clientHeight, radius: Number.parseFloat(style.borderTopLeftRadius) };
|
||||
});
|
||||
expect(sendGeometry.width).toBe(32);
|
||||
expect(sendGeometry.height).toBe(32);
|
||||
expect(sendGeometry.radius).toBeGreaterThanOrEqual(sendGeometry.width / 2);
|
||||
const toolbarPositions = await voiceComposer.locator('[aria-label="添加文件或图片"], [aria-label="语音输入"], [aria-label="发送"]').evaluateAll(elements =>
|
||||
Object.fromEntries(elements.map(element => {
|
||||
const bounds = element.getBoundingClientRect();
|
||||
return [element.getAttribute('aria-label'), { left: bounds.left, right: bounds.right }];
|
||||
})));
|
||||
expect(toolbarPositions['语音输入'].left).toBeGreaterThan(toolbarPositions['添加文件或图片'].right);
|
||||
expect(toolbarPositions['发送'].left).toBeGreaterThan(toolbarPositions['语音输入'].right);
|
||||
expect(toolbarPositions['发送'].left - toolbarPositions['语音输入'].right).toBeCloseTo(4, 1);
|
||||
await voiceDraft.fill('原来的想法');
|
||||
await voiceComposer.getByRole('button', { name: '语音输入', exact: true }).click();
|
||||
await expect(voiceComposer.getByRole('button', { name: '停止语音输入' })).toBeVisible();
|
||||
await voiceDraft.fill('录音时继续编辑');
|
||||
await expect(voiceComposer.getByRole('button', { name: '发送', exact: true })).toBeDisabled();
|
||||
await voiceDraft.press('Enter');
|
||||
await voiceComposer.getByRole('button', { name: '停止语音输入' }).click();
|
||||
await expect(voiceComposer.getByRole('button', { name: '正在识别语音' })).toBeDisabled();
|
||||
await voiceDraft.fill('识别时继续编辑');
|
||||
await expect(voiceDraft).toHaveValue('识别时继续编辑\n加上语音描述');
|
||||
await expect(voiceDraft).toHaveAttribute('rows', '2');
|
||||
expect((await fixtureState()).speechRequests).toBe(1);
|
||||
expect(await messageOperations()).toEqual([]);
|
||||
await page.screenshot({ path: testInfo.outputPath('cloud-agent-published-voice-composer.png') });
|
||||
await voiceDraft.fill('');
|
||||
const expectCleanChat = async () => {
|
||||
await expect(page.getByRole('navigation', { name: '对话来源', exact: true })).toHaveCount(0);
|
||||
await expect(page.getByRole('textbox', { name: '名称', exact: true })).toHaveCount(0);
|
||||
@@ -331,10 +407,10 @@ test('personal cloud Agent identity, configuration and separate published chat i
|
||||
const userBubble = formalChat.locator('.agent-chat-message[data-role="user"]');
|
||||
await expect(userBubble).toHaveText(/先给我一些创作建议/);
|
||||
await expect(userBubble.locator('time')).toHaveAttribute('datetime', '2026-10-13T09:24:30.000Z');
|
||||
await expect(userBubble.locator('time')).toHaveAccessibleName(/^发送时间:/);
|
||||
await expect(userBubble.locator('time')).toHaveAttribute('aria-label', /^发送时间:/);
|
||||
const answerTime = formalChat.locator('.agent-chat-message[data-role="assistant"] time');
|
||||
await expect(answerTime).toHaveAttribute('datetime', '2026-10-13T09:25:40.000Z');
|
||||
await expect(answerTime).toHaveAccessibleName(/^回复时间:/);
|
||||
await expect(answerTime).toHaveAttribute('aria-label', /^回复时间:/);
|
||||
const bubbleBody = userBubble.locator('.agent-chat-message-body');
|
||||
const bubbleStyle = await bubbleBody.evaluate(element => {
|
||||
const style = getComputedStyle(element);
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
Reference in new issue
Block a user