| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138 |
- import assert from 'node:assert/strict';
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
- import { chromium } from 'playwright';
- const accounts = JSON.parse(await readFile(new URL('../../.work/frontend-accounts.json', import.meta.url), 'utf8'));
- const base = process.env.FRONTEND_URL || 'http://127.0.0.1:18762';
- const browser = await chromium.launch({ headless: true, executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE });
- const context = await browser.newContext();
- const page = await context.newPage();
- const errors = [], checks = [];
- page.on('pageerror', error => errors.push(error.message));
- let customerToken;
- async function settle() {
- await page.waitForFunction(() => !document.querySelector('.merchant-loading'));
- }
- async function route(path) {
- await page.evaluate(value => { location.hash = `/${value}`; }, path);
- await page.waitForURL(`**/#/${path}`);
- await settle();
- }
- async function login(username) {
- await page.goto(`${base}/#/merchant/login`);
- await page.getByLabel('商家标识').fill(accounts.tenant_id);
- await page.getByLabel('员工账户').fill(username);
- await page.getByLabel('密码', { exact: true }).fill(accounts.password);
- await page.getByRole('button', { name: '登录', exact: true }).click();
- await page.locator('.admin-shell').waitFor();
- await settle();
- }
- async function confirm() {
- await page.getByRole('dialog').getByRole('button', { name: '确认操作', exact: true }).click();
- await page.getByRole('dialog').waitFor({ state: 'hidden' });
- await settle();
- }
- async function customerRequest(method, path, data, expected = 200) {
- const response = await context.request.fetch(`${base}${path}`, {
- method, headers: { Authorization: `Bearer ${customerToken}`, 'Idempotency-Key': crypto.randomUUID() }, data,
- });
- assert.equal(response.status(), expected, await response.text());
- return expected === 204 ? null : response.json();
- }
- async function openQueueConversation(id) {
- await route('merchant/service/queue');
- for (let index = 0; index < 50; index += 1) {
- const row = page.locator('.session-row').filter({ hasText: id });
- if (await row.count()) { await row.click(); await page.waitForURL('**/#/merchant/service/conversation'); await settle(); return; }
- 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); await settle();
- }
- throw new Error('会话队列超过本次验证的 50 页限制');
- }
- try {
- const response = await context.request.post(`${base}/openapi/v1/auth/sessions`, {
- data: { tenant_id: accounts.tenant_id, username: 'customer01', password: accounts.password },
- });
- assert.equal(response.status(), 200);
- customerToken = (await response.json()).access_token;
- // 使用真实客户接口准备待整理工单,全程不发送 AGENT 模式客户消息。
- const conversation = await customerRequest('POST', '/openapi/v1/conversations', { store_id: accounts.store_id }, 201);
- const order = await customerRequest('GET', `/openapi/v1/orders/${accounts.order_id}`);
- const original = await customerRequest('POST', '/openapi/v1/after-sales/proposals', {
- order_id: order.order_id, item_id: order.item_id, quantity: 1, reason: `客服整理浏览器验收 ${Date.now()}`,
- }, 201);
- await login('staff01');
- await openQueueConversation(conversation.conversation_id);
- await page.locator('.merchant-pages').getByText(original.citation, { exact: true }).waitFor();
- assert.match(await page.locator('.merchant-pages').innerText(), new RegExp(order.order_id));
- await page.getByRole('button', { name: '接管会话', exact: true }).click();
- await confirm();
- const reply = `客服已接收本次资料 ${Date.now()}`;
- await page.getByLabel('人工回复').fill(reply);
- const replied = page.waitForResponse(item => item.request().method() === 'POST' && item.url().endsWith(`/conversations/${conversation.conversation_id}/messages`));
- await page.getByRole('button', { name: '发送回复', exact: true }).click();
- assert.equal((await replied).status(), 201);
- await page.locator('.message').filter({ hasText: reply }).waitFor();
- const events = await customerRequest('GET', `/openapi/v1/conversations/${conversation.conversation_id}/messages?limit=100`);
- assert(events.items.some(item => item.type === 'message.staff' && item.payload.text === reply));
- checks.push('客服从真实队列恢复客户订单与当前工单,接管回复对客户可见');
- await page.getByRole('button', { name: '整理换货方案', exact: true }).click();
- await page.waitForURL('**/#/merchant/service/draft');
- await settle();
- assert.equal(await page.getByLabel('故障描述').inputValue(), original.reason);
- const reason = `客服已核查按键故障 ${Date.now()}`;
- await page.getByLabel('故障描述').fill(reason);
- const revised = page.waitForResponse(item => item.request().method() === 'POST' && item.url().endsWith(`/proposals/${original.proposal_id}/revisions`));
- await page.getByRole('button', { name: '保存并交客户检查', exact: true }).click();
- const revisionResponse = await revised;
- assert.equal(revisionResponse.status(), 200, await revisionResponse.text());
- const ticket = await revisionResponse.json();
- assert.equal(ticket.proposal_version, original.proposal_version + 1);
- assert.equal(ticket.submitted_by, accounts.actors.staff01);
- await page.waitForURL('**/#/merchant/service/ticket');
- await settle();
- assert.match(await page.locator('.merchant-pages').innerText(), new RegExp(reason));
- const persisted = await customerRequest('GET', `/openapi/v1/tickets/${ticket.ticket_id}`);
- assert.equal(persisted.reason, reason);
- const confirmed = await customerRequest('POST', `/openapi/v1/after-sales/proposals/${ticket.proposal_id}/confirm`, {
- proposal_version: ticket.proposal_version, expected_ticket_version: ticket.version,
- });
- assert.equal(confirmed.status, 'WAITING_REVIEW');
- checks.push('客服修订生成新方案版本,客户读取相同正文并本人确认');
- await route('merchant/service/conversation');
- await page.getByRole('button', { name: '结束接管', exact: true }).click();
- await confirm();
- assert.equal(await page.getByRole('button', { name: '发送回复', exact: true }).isDisabled(), true);
- await login('supervisor01');
- await route('merchant/approval/approvals');
- const row = page.locator('tr').filter({ hasText: ticket.ticket_id });
- await row.getByRole('button', { name: '查看并审批', exact: true }).click();
- await page.waitForURL('**/#/merchant/approval/review');
- await settle();
- await page.locator('.el-select').filter({ has: page.getByRole('combobox', { name: '审批决定', exact: true }) }).click();
- await page.getByRole('option', { name: '拒绝', exact: true }).click();
- await page.getByLabel('审批意见').fill('已核查浏览器验收申请,本次结束申请。');
- await page.locator('.merchant-pages .el-checkbox').click();
- assert.equal(await page.getByRole('checkbox').isChecked(), true);
- await page.getByRole('button', { name: '提交审批决定', exact: true }).click();
- await confirm();
- const result = await customerRequest('GET', `/openapi/v1/tickets/${ticket.ticket_id}`);
- assert.equal(result.status, 'REJECTED');
- assert.equal(result.decision_reason, '已核查浏览器验收申请,本次结束申请。');
- checks.push('客服释放接管后不能回复,主管审批结果持久保存并向客户返回');
- assert.deepEqual(errors, []);
- await mkdir(new URL('../../.work/frontend-verification/', import.meta.url), { recursive: true });
- await writeFile(new URL('../../.work/frontend-verification/merchant-conversation.json', import.meta.url), JSON.stringify({
- date: new Date().toISOString(), checks, browserErrors: errors, generatedModelCalls: 0,
- }, null, 2));
- console.log(checks.join('\n'));
- } finally {
- if (customerToken) await customerRequest('DELETE', '/openapi/v1/auth/sessions/current', undefined, 204);
- await browser.close();
- }
|