| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153 |
- import assert from 'node:assert/strict';
- import { readFile, writeFile, mkdir } from 'node:fs/promises';
- import { chromium } from 'playwright';
- // 真实客户与客服在 HUMAN 会话中发送消息;本检查不启动生成模型任务。
- const base = process.env.FRONTEND_URL || 'http://127.0.0.1:18762';
- const accounts = JSON.parse(await readFile(new URL('../../.work/frontend-accounts.json', import.meta.url), 'utf8'));
- const platformAccount = JSON.parse(await readFile(new URL('../../.work/platform-accounts.json', import.meta.url), 'utf8'));
- const browser = await chromium.launch({ headless: true, executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE || 'C:/Users/Administrator/AppData/Local/ms-playwright/chromium-1228/chrome-win64/chrome.exe' });
- const context = await browser.newContext({ viewport: { width: 390, height: 844 }, timezoneId: 'Asia/Shanghai' });
- const page = await context.newPage();
- page.setDefaultTimeout(15000);
- const errors = [], checks = [], tokens = [];
- page.on('pageerror', failure => errors.push(failure.message));
- let conversation;
- async function request(method, path, token, data, expected = 200) {
- const response = await context.request.fetch(`${base}${path}`, { method, data, headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), 'Idempotency-Key': crypto.randomUUID() } });
- assert.equal(response.status(), expected, `${method} ${path}: ${await response.text()}`);
- return expected === 204 ? null : response.json();
- }
- async function loginToken(username) {
- const result = await request('POST', '/openapi/v1/auth/sessions', null, { tenant_id: accounts.tenant_id, username, password: accounts.password });
- tokens.push(result.access_token);
- return result.access_token;
- }
- async function loginUi(product, username, password) {
- await page.goto(`${base}/#/${product}/login`);
- if (product === 'merchant') await page.getByLabel('商家标识', { exact: true }).fill(accounts.tenant_id);
- await page.getByLabel(product === 'customer' ? '账户' : '员工账户', { exact: true }).fill(username);
- await page.getByLabel('密码', { exact: true }).fill(password);
- if (product === 'customer') await page.getByRole('checkbox').check();
- await page.getByRole('button', { name: product === 'customer' ? '登录并继续' : '登录', exact: true }).click();
- await page.waitForURL(url => !url.hash.endsWith('/login'));
- }
- async function route(path) {
- await page.evaluate(value => { location.hash = `/${value}`; }, path);
- await page.waitForURL(`**/#/${path}`);
- await page.waitForFunction(() => !document.querySelector('.merchant-loading, .content[aria-busy="true"]'));
- }
- async function checkCollapsed(locator) {
- const content = locator.locator('.expandable-text__content');
- await locator.getByRole('button', { name: '展开消息正文', exact: true }).waitFor();
- assert.ok((await content.boundingBox()).height <= 241);
- await locator.getByRole('button', { name: '展开消息正文', exact: true }).click();
- assert.ok((await content.boundingBox()).height > 240);
- assert.equal(await content.evaluate(node => getComputedStyle(node).overflowY), 'hidden');
- await locator.getByRole('button', { name: '收起消息正文', exact: true }).click();
- assert.ok((await content.boundingBox()).height <= 241);
- }
- try {
- const customer = await loginToken('customer01');
- const staff = await loginToken('staff01');
- // H5 根据当前服务的 UUID 游标顺序恢复首个会话,验证使用相同公开查询结果。
- const conversations = await request('GET', `/openapi/v1/conversations?store_id=${accounts.store_id}&limit=20`, customer);
- conversation = conversations.items[0] || await request('POST', '/openapi/v1/conversations', customer, { store_id: accounts.store_id }, 201);
- const control = await request('POST', `/openapi/v1/conversations/${conversation.conversation_id}/takeover`, staff, { expected_control_version: conversation.control_version });
- const version = control.payload.control_version;
- const marker = `消息展示检查 ${Date.now()}`;
- const longMessage = Array.from({ length: 32 }, (_, index) => `${marker} 第${index + 1}项:请保留订单编号并核对商品信息,处理结果会通过当前会话通知。`).join('\n');
- for (let index = 0; index < 12; index++) {
- await request('POST', `/openapi/v1/conversations/${conversation.conversation_id}/messages`, staff, { text: index === 2 ? longMessage : `${marker} 客服回复 ${index + 1}`, expected_control_version: version }, 201);
- }
- await request('POST', `/openapi/v1/conversations/${conversation.conversation_id}/messages`, customer, { text: `${marker} 客户已补充资料`, expected_control_version: version }, 201);
- const events = await request('GET', `/openapi/v1/conversations/${conversation.conversation_id}/messages?limit=100`, customer);
- assert.ok(events.items.every(event => event.created_at && Number.isFinite(Date.parse(event.created_at))), '历史消息提供真实时间');
- await loginUi('customer', 'customer01', accounts.password);
- await page.locator('.mobile[data-page="chat"] .message').filter({ hasText: `${marker} 客户已补充资料` }).waitFor();
- const h5Long = page.locator('.mobile .message').filter({ hasText: `${marker} 第1项` });
- await checkCollapsed(h5Long);
- const visibleText = await page.locator('.mobile').innerText();
- assert.equal(visibleText.includes('control.changed'), false);
- assert.equal(visibleText.includes(conversation.conversation_id), false);
- checks.push('H5 真实消息身份、时间、240px正文展开与内部编号隐藏');
- const history = page.getByRole('log', { name: '咨询记录', exact: true });
- await history.evaluate(node => { node.scrollTop = 0; });
- await page.waitForFunction(() => document.querySelector('.customer-chat-history').scrollTop === 0);
- await request('POST', `/openapi/v1/conversations/${conversation.conversation_id}/messages`, staff, { text: `${marker} 新到消息`, expected_control_version: version }, 201);
- const latest = page.getByRole('button', { name: /查看 \d+ 条新消息/ });
- await latest.waitFor();
- assert.ok(await history.evaluate(node => node.scrollTop < 2), '新消息保持历史阅读位置');
- await latest.click();
- assert.ok(await history.evaluate(node => node.scrollHeight - node.scrollTop - node.clientHeight < 2), '返回最新消息');
- checks.push('H5 阅读历史时新消息提示与主动返回最新消息');
- for (const [width, height] of [[320, 568], [390, 844], [844, 390], [1440, 900]]) {
- await page.setViewportSize({ width, height });
- assert.ok(await page.locator('html').evaluate(node => node.scrollWidth <= node.clientWidth + 1), `${width}px 会话宽度`);
- assert.ok(await history.evaluate(node => node.clientHeight >= 80), `${width}×${height} 消息阅读区域至少80px`);
- const send = page.getByRole('button', { name: '发送', exact: true });
- await send.scrollIntoViewIfNeeded();
- const box = await send.boundingBox();
- assert.ok(box.y >= 0 && box.y + box.height <= height + 1, `${width}px 发送操作可访问`);
- }
- checks.push('H5 四种视口下长会话与发送操作可访问');
- const current = await request('GET', `/openapi/v1/conversations/${conversation.conversation_id}`, customer);
- assert.equal(current.active_run_id, null, 'HUMAN 消息未创建生成任务');
- await page.setViewportSize({ width: 1440, height: 1000 });
- await loginUi('merchant', 'staff01', accounts.password);
- await page.getByRole('button', { name: '我的会话', exact: true }).click();
- let selected = false;
- for (let index = 0; index < 50; index++) {
- const row = page.locator('.session-row').filter({ hasText: conversation.conversation_id.slice(-8) });
- if (await row.count()) { await row.click(); selected = true; break; }
- const more = page.getByRole('button', { name: '加载更多', exact: true });
- assert.equal(await more.count(), 1, '会话分页可以找到本次会话');
- const response = page.waitForResponse(item => item.request().method() === 'GET' && item.url().includes('/openapi/v1/conversations?'));
- await more.click();
- assert.equal((await response).status(), 200);
- }
- assert.ok(selected);
- await page.locator('.merchant-pages .message').filter({ hasText: `${marker} 客户已补充资料` }).waitFor();
- await checkCollapsed(page.locator('.merchant-pages .message').filter({ hasText: `${marker} 第1项` }));
- assert.ok(await page.locator('.merchant-pages time').count() > 0, '商家显示真实时间');
- checks.push('商家真实队列选择、相同消息正文及展开收起');
- await loginUi('platform', platformAccount.username, platformAccount.password);
- await route('platform/deliveries');
- await page.locator('.platform-pages > [role="status"]').waitFor({ state: 'hidden' });
- const details = page.locator('.platform-pages').getByRole('button', { name: '查看详情', exact: true });
- const detailCount = await details.count();
- for (let index = 0; index < Math.min(detailCount, 2); index++) {
- const row = details.nth(index).locator('xpath=ancestor::tr');
- const event = await row.locator('.platform-identifier').innerText();
- await details.nth(index).click();
- const dialog = page.getByRole('dialog', { name: '投递详情', exact: true });
- await dialog.getByText(event, { exact: true }).waitFor();
- assert.ok((await dialog.locator('.platform-delivery-detail').boundingBox()).height <= 561);
- await dialog.getByRole('button', { name: '关闭详情', exact: true }).click();
- }
- if (detailCount) checks.push(`平台从列表查看对应投递详情:${Math.min(detailCount, 2)} 条真实记录`);
- else { await page.getByText('暂无结果投递记录。', { exact: true }).waitFor(); checks.push('平台投递列表空内容提示;有记录详情另由真实投递专项验证'); }
- for (const name of ['tenants', 'audit', 'health']) {
- await route(`platform/${name}`);
- await page.locator('.platform-pages > [role="status"]').waitFor({ state: 'hidden' });
- for (const table of await page.locator('.platform-pages .el-table').all()) {
- assert.ok((await table.boundingBox()).height <= 561, `${name} 表格高度`);
- }
- }
- checks.push('平台商家、审计及监控表格最高560px');
- assert.deepEqual(errors, []);
- await mkdir(new URL('../../.work/frontend-verification/', import.meta.url), { recursive: true });
- await writeFile(new URL('../../.work/frontend-verification/presentation.json', import.meta.url), JSON.stringify({ date: new Date().toISOString(), checks, browserErrors: errors, generatedModelCalls: 0, screenshots: 0 }, null, 2));
- console.log(JSON.stringify({ checks, browserErrors: errors, generatedModelCalls: 0 }, null, 2));
- } finally {
- // 接管恢复使用当前版本;本次验证不会提交客户消息给生成模型。
- if (conversation && tokens.length > 1) {
- const current = await request('GET', `/openapi/v1/conversations/${conversation.conversation_id}`, tokens[0]);
- if (current.control_owner === 'HUMAN') await request('POST', `/openapi/v1/conversations/${conversation.conversation_id}/release`, tokens[1], { expected_control_version: current.control_version });
- }
- for (const token of tokens) await request('DELETE', '/openapi/v1/auth/sessions/current', token, undefined, 204);
- await browser.close();
- }
|