import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; // 从浏览器可见页面验证业务行为,不读取组件状态或替换 HTTP 响应。 export async function checkCustomer(page, baseUrl, accounts) { const results = []; const content = page.locator('.mobile .content'); async function navigate(target) { await page.evaluate(value => { window.location.hash = `/customer/${value}`; }, target); await page.waitForURL(new RegExp(`#/?customer/${target}$`)); await page.locator(`.mobile[data-page="${target}"]`).waitFor(); await page.waitForFunction(() => !document.querySelector('.mobile .content[aria-busy="true"]')); } async function click(name) { await content.getByRole('button', { name, exact: true }).click(); } async function confirm() { await page.getByRole('dialog').getByRole('button', { name: '确认操作', exact: true }).click(); await page.getByRole('dialog').waitFor({ state: 'hidden' }); } async function expectText(text) { await content.getByText(text, { exact: false }).first().waitFor(); } await page.goto(`${baseUrl}/#customer/login`); await content.getByLabel('账户', { exact: true }).fill('customer01'); await content.getByLabel('密码', { exact: true }).fill(`${accounts.password}-invalid`); await content.getByRole('checkbox').check(); await click('登录并继续'); await content.getByRole('alert').waitFor(); assert.equal(await content.getByLabel('密码', { exact: true }).inputValue(), ''); assert.equal(await content.getByLabel('账户', { exact: true }).inputValue(), 'customer01'); await content.getByLabel('密码', { exact: true }).fill(accounts.password); await click('登录并继续'); await page.waitForURL(/#\/?customer\/chat$/); await expectText('已登录'); results.push('H5 真实身份登录与返回咨询'); // 限制真实网络速度,检查旧页面仍在读取时进入新页面的行为,不替换任何服务响应。 await navigate('profile'); const network = await page.context().newCDPSession(page); await network.send('Network.enable'); await network.send('Network.emulateNetworkConditions', { offline: false, latency: 180, downloadThroughput: 375000, uploadThroughput: 375000 }); try { const readingChat = page.waitForRequest(request => request.method() === 'GET' && request.url().includes('/openapi/v1/conversations')); await page.evaluate(() => { location.hash = '/customer/chat'; }); await readingChat; await navigate('orders'); await content.locator('.order-card').filter({ hasText: accounts.order_id }).waitFor(); } finally { await network.send('Network.emulateNetworkConditions', { offline: false, latency: 0, downloadThroughput: -1, uploadThroughput: -1 }); await network.detach(); } results.push('真实慢速网络中从咨询快速进入订单,订单请求及内容正常'); const pages = ['home','categories','product','cart','profile','messages','address','favorites','settings','chat','orders','order','policy','apply','proposal','ticket','memory','identity','login']; for (const target of pages) { await navigate(target); assert.equal(await content.getByRole('alert').count(), 0, `${target} 页面出现服务错误`); assert.ok((await page.locator('.mobile-header strong').last().innerText()).length > 0, `${target} 缺少标题`); for (const width of [390, 1440]) { await page.setViewportSize({ width, height: 900 }); const geometry = await page.locator('body').evaluate(element => ({ width: document.documentElement.clientWidth, scroll: element.scrollWidth })); assert.ok(geometry.scroll <= geometry.width + 1, `${target} ${width}px 页面横向溢出`); } } results.push('19 个 H5 页面可访问,390px 与 1440px 宽度无横向溢出'); const catalogResponse = await page.request.get(`${baseUrl}/openapi/v1/stores/${accounts.store_id}/catalog?limit=100`); assert.equal(catalogResponse.status(), 200); const catalog = await catalogResponse.json(); await navigate('login'); assert.equal(await content.locator('.customer-login-store strong').innerText(), catalog.store_name); await navigate('home'); assert.equal(await content.locator('.shop-banner small').innerText(), catalog.store_name); assert.deepEqual(await content.locator('.goods-info strong').allTextContents(), catalog.items.slice(0, 2).map(item => item.name)); results.push('首页商品、分类与登录店铺名称来自实际目录接口'); await page.locator('.shop-tabs').getByRole('tab', { name: '分类', exact: true }).click(); await page.waitForURL(/#\/?customer\/categories$/); await page.locator('.shop-tabs').getByRole('tab', { name: '首页', exact: true }).click(); await page.waitForURL(/#\/?customer\/home$/); await content.locator('.ai-entry').click(); await page.waitForURL(/#\/?customer\/chat$/); await page.waitForFunction(() => !document.querySelector('.mobile .content[aria-busy="true"]')); const humanButton = content.getByRole('button', { name: '申请人工协助', exact: true }); if (!await humanButton.isDisabled()) await humanButton.click(); await expectText('等待客服接管'); assert.equal(await content.getByRole('button', { name: '申请人工协助', exact: true }).isDisabled(), true); await navigate('profile'); await navigate('chat'); await expectText('等待客服接管'); results.push('人工协助真实持久登记、重新进入恢复及重复操作禁用'); // 使用实际客服身份接管会话后验证客户消息,全程不产生生成模型请求。 await navigate('profile'); const conversationResponse = page.waitForResponse(response => response.request().method() === 'GET' && /\/conversations\/[0-9a-f-]+$/.test(new URL(response.url()).pathname)); await navigate('chat'); const conversation = await (await conversationResponse).json(); const staffAuth = await page.request.post(`${baseUrl}/openapi/v1/auth/sessions`, { data: { tenant_id: accounts.tenant_id, username: 'staff01', password: accounts.password } }); assert.equal(staffAuth.status(), 200); const staffToken = (await staffAuth.json()).access_token; const staffHeaders = { Authorization: `Bearer ${staffToken}` }; const conversationPath = `${baseUrl}/openapi/v1/conversations/${conversation.conversation_id}`; let taken = false; try { const takeover = await page.request.post(`${conversationPath}/takeover`, { headers: { ...staffHeaders, 'Idempotency-Key': crypto.randomUUID() }, data: { expected_control_version: conversation.control_version } }); assert.equal(takeover.status(), 200, await takeover.text()); taken = true; await navigate('orders'); await content.locator('.order-card').filter({ hasText: accounts.order_id }).getByRole('button', { name: '联系 AI 客服', exact: true }).click(); await expectText('人工客服正在为你服务'); const question = `请核实当前订单的服务进度 ${Date.now()}`; await content.getByLabel('补充问题', { exact: true }).fill(question); const sent = page.waitForResponse(response => response.request().method() === 'POST' && response.url() === `${conversationPath}/messages`); await click('发送'); const saved = await sent; assert.equal(saved.status(), 201, await saved.text()); const event = await saved.json(); assert.equal(event.payload.text, `咨询对象:订单 ${accounts.order_id}\n${question}`); assert.equal(event.payload.run_id, null); await navigate('profile'); await navigate('chat'); await expectText(question); results.push('订单咨询上下文和客户问题真实保存,重新进入恢复同一消息'); } finally { if (taken) { const latest = await page.request.get(conversationPath, { headers: staffHeaders }); assert.equal(latest.status(), 200); const release = await page.request.post(`${conversationPath}/release`, { headers: { ...staffHeaders, 'Idempotency-Key': crypto.randomUUID() }, data: { expected_control_version: (await latest.json()).control_version } }); assert.equal(release.status(), 200, await release.text()); } const logout = await page.request.delete(`${baseUrl}/openapi/v1/auth/sessions/current`, { headers: staffHeaders }); assert.equal(logout.status(), 204); } // 开发验收订单可能保留已登记的申请,通过本人确认取消后创建本次独立方案。 let prepared = false; let cancelled = 0; for (let attempt = 0; attempt < 100; attempt++) { await navigate('orders'); const order = content.locator('.order-card').filter({ hasText: accounts.order_id }); await order.getByRole('button', { name: '查看订单', exact: true }).click(); await expectText(accounts.order_id); await click('填写换货申请'); await page.waitForFunction(() => !document.querySelector('.mobile .content[aria-busy="true"]')); const createButton = content.getByRole('button', { name: '生成方案并检查', exact: true }); if (await createButton.count() && await createButton.isEnabled()) { prepared = true; break; } await navigate('proposal'); assert.equal(await content.getByRole('button', { name: '取消申请', exact: true }).isEnabled(), true, '验收订单存在不可取消申请,须准备可创建方案的真实订单'); const cancelling = page.waitForResponse(response => response.request().method() === 'POST' && /\/tickets\/[^/]+\/cancel$/.test(new URL(response.url()).pathname)); await click('取消申请'); await confirm(); const cancelledResponse = await cancelling; assert.equal(cancelledResponse.status(), 200, await cancelledResponse.text()); assert.equal((await cancelledResponse.json()).status, 'CANCELLED'); cancelled++; } assert.equal(prepared, true, '验收订单在限定次数内未进入可创建方案状态'); if (cancelled) results.push(`通过本人确认取消 ${cancelled} 个既有验收申请,准备独立方案`); await content.getByLabel('申请数量', { exact: true }).fill('1'); await content.getByLabel('故障描述', { exact: true }).fill('键盘空格键无法使用,申请换货。'); const creatingProposal = page.waitForResponse(response => response.request().method() === 'POST' && new URL(response.url()).pathname === '/openapi/v1/after-sales/proposals'); await click('生成方案并检查'); const proposalResponse = await creatingProposal; assert.equal(proposalResponse.status(), 201, await proposalResponse.text()); const proposal = await proposalResponse.json(); assert.equal(proposal.proposal_version, 1); assert.equal(proposal.order_id, accounts.order_id); await page.waitForURL(/#\/?customer\/proposal$/); await expectText('待客户确认'); await content.getByRole('checkbox').check(); await click('确认并提交审批'); await page.waitForURL(/#\/?customer\/ticket$/); await expectText('等待主管审批'); await click('修订方案'); await content.getByLabel('故障描述', { exact: true }).fill('键盘空格键和回车键无法使用,申请换货。'); await click('保存新版本并检查'); await page.waitForURL(/#\/?customer\/proposal$/); await expectText('方案版本:V2'); await click('取消申请'); await confirm(); await expectText('申请已取消'); assert.equal(await content.getByRole('button', { name: '取消申请', exact: true }).isDisabled(), true); results.push('真实订单查询、方案创建、本人确认、修订失效及取消'); await navigate('memory'); const authorize = content.getByRole('button', { name: '授权保存', exact: true }); if (await authorize.count()) { await authorize.click(); await confirm(); } await content.locator('.memory-style-field').getByRole('button').click(); await page.getByRole('menuitem', { name: '详细回复', exact: true }).click(); await content.locator('.memory-language-field').getByRole('button').click(); await page.getByRole('menuitem', { name: 'English', exact: true }).click(); await click('保存沟通习惯'); await expectText('已保存:详细回复 / English'); await navigate('profile'); await navigate('memory'); await expectText('已保存:详细回复 / English'); await click('清空已保存内容'); await confirm(); await expectText('当前没有已保存的沟通习惯'); assert.match(await content.locator('.memory-style-field').innerText(), /简洁回复/); assert.match(await content.locator('.memory-language-field').innerText(), /中文/); await click('撤销授权'); await confirm(); await expectText('尚未授权'); results.push('真实沟通习惯授权、保存、清空及撤销'); await navigate('identity'); if (!await content.getByText('Webhook 渠道已关联', { exact: true }).count()) { const creating = page.waitForResponse(response => response.request().method() === 'POST' && response.url().includes('/me/identity-challenges')); await click('生成验证代码'); const created = await creating; if (created.status() === 409) { assert.equal((await created.json()).code, 'CHANNEL_UNAVAILABLE'); await expectText('渠道尚未启用'); assert.equal(await content.getByRole('button', { name: '确认关联', exact: true }).isDisabled(), true); results.push('渠道未启用时真实拒绝验证且关联按钮保持禁用'); return results; } assert.equal(created.status(), 200, await created.text()); await content.locator('.identity-code').waitFor(); assert.equal(await content.getByRole('button', { name: '确认关联', exact: true }).isDisabled(), true); const challengeId = await content.locator('.identity-code').innerText(); const credentials = JSON.parse(await readFile(new URL('../../.work/storefront-channel.json', import.meta.url), 'utf8')); const response = await page.request.post(`${baseUrl}/openapi/v1/auth/sessions`, { data: { tenant_id: credentials.tenant_id, username: credentials.username, password: credentials.password } }); assert.equal(response.status(), 200); const token = (await response.json()).access_token; const verified = await page.request.post(`${baseUrl}/openapi/v1/channel/identity-challenges/${challengeId}/verify`, { headers: { Authorization: `Bearer ${token}` }, data: { store_id: credentials.store_id, channel: 'webhook', external_subject: 'frontend-channel-customer01', display_name: '渠道客户 customer01' }, }); assert.equal(verified.status(), 200, await verified.text()); await click('查询验证结果'); await expectText('渠道客户 customer01'); await click('确认关联'); await confirm(); await page.request.delete(`${baseUrl}/openapi/v1/auth/sessions/current`, { headers: { Authorization: `Bearer ${token}` } }); } await expectText('Webhook 渠道已关联'); await navigate('profile'); await navigate('identity'); await expectText('Webhook 渠道已关联'); results.push('真实渠道服务验证、本人再次确认及持久关联恢复'); return results; }