| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889 |
- import { chromium } from 'playwright';
- import { readFile, writeFile, mkdir } from 'node:fs/promises';
- import assert from 'node:assert/strict';
- // 通过实际浏览器、HTTP API 与持久数据验证,生成模型调用次数为零。
- const credentials = 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({ executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE || 'C:/Users/Administrator/AppData/Local/ms-playwright/chromium-1228/chrome-win64/chrome.exe', headless: true });
- const results = [], errors = [];
- const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
- const page = await context.newPage();
- page.on('pageerror', error => errors.push(error.message));
- const content = page.locator('.merchant-pages');
- async function waitLoaded() { await page.waitForTimeout(200); await page.getByText('正在读取数据…', { exact: true }).waitFor({ state: 'hidden' }); }
- async function route(role, target) { const path = role === 'staff' ? `merchant/service/${target}` : role === 'supervisor' ? `merchant/approval/${target}` : `merchant/${target}`; await page.evaluate(hash => { location.hash = hash; }, `#/${path}`); await waitLoaded(); }
- async function login(account) { await page.goto(`${base}/#/merchant/login`); await page.getByLabel('商家标识').fill(credentials.tenant_id); await page.getByLabel('员工账户').fill(account); await page.getByLabel('密码', { exact: true }).fill(credentials.password); await page.getByRole('button', { name: '登录', exact: true }).click(); await page.locator('.admin-shell,.login-panel [role="alert"]').first().waitFor(); assert.equal(await page.locator('.login-panel [role="alert"]').count(), 0, await page.locator('body').innerText()); await waitLoaded(); }
- async function confirm() { await page.getByRole('dialog').getByRole('button', { name: '确认操作', exact: true }).click(); await page.getByRole('dialog').waitFor({ state: 'hidden' }); await waitLoaded(); }
- async function check(name, action) { await action(); results.push(name); console.log(`通过:${name}`); }
- const routes = {
- merchant: ['overview','products','orders','after-sales','customers','marketing','service','knowledge','rules','release','billing','access','channels','notifications','notification-detail'],
- staff: ['queue','ticket','customers','reference','quick-replies','statistics','notifications','conversation','draft','notification-detail'],
- supervisor: ['approvals','history','team','statistics','notifications','review','notification-detail'],
- };
- try {
- for (const [role, account] of [['merchant','admin01'],['staff','staff01'],['supervisor','supervisor01']]) {
- await login(account);
- await check(`${role} 菜单与 ${routes[role].length} 个页面`, async () => {
- for (const target of routes[role]) {
- await route(role, target); assert.equal(await content.count(), 1, `${role}/${target}`);
- assert.equal(await content.locator('.error-state').count(), 0, `${role}/${target} 加载失败:${await content.innerText()}`);
- assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth + 1), false, `${role}/${target} 页面宽度`);
- }
- });
- await check(`${role} 通知详情、未读与业务跳转`, async () => {
- await route(role, 'notifications');
- assert.doesNotMatch(await content.innerText(), /mock/);
- if (await content.getByRole('button', { name: '查看详情', exact: true }).count()) {
- await content.getByRole('button', { name: '查看详情', exact: true }).first().click();
- await waitLoaded(); assert.match(await content.innerText(), /已读/);
- await content.locator('.actions button').first().click(); await waitLoaded();
- assert.equal(await content.locator('.error-state').count(),0,await content.innerText());
- assert.doesNotMatch(page.url(),/notification-detail$/);
- await route(role,'notifications');
- }
- await content.getByRole('button', { name: '未读', exact: true }).click();
- await waitLoaded();
- if (await content.getByRole('button', { name: '全部标为已读' }).isEnabled()) { const marked = page.waitForResponse(response => response.request().method() === 'POST' && response.url().endsWith('/notifications/read-all')); await content.getByRole('button', { name: '全部标为已读' }).click(); await confirm(); const response = await marked; assert.equal(response.status(),200,await response.text()); await waitLoaded(); }
- assert.match(await content.innerText(), /暂无未读通知/);
- assert.match(await page.locator('.admin-session').innerText(), /消息通知(\d+)/);
- });
- await page.setViewportSize({ width: 390, height: 844 });
- await check(`${role} 手机宽度全部页面`, async () => { for (const target of routes[role]) { await route(role, target); assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth + 1), false, `${role}/${target}`); } });
- await page.setViewportSize({ width: 1440, height: 1000 });
- }
- await login('admin01');
- await check('管理员员工邀请持久化、停用确认与恢复', async () => {
- await route('merchant', 'access'); const account = `browser.staff.${Date.now()}`; await content.getByLabel('员工账户').fill(account); await content.getByRole('button', { name: '保存邀请与授权' }).click(); await page.locator('.app-notice').filter({hasText:'邀请与授权已登记'}).waitFor(); assert.match(await content.innerText(), /已登记,等待身份接入/);
- await page.reload(); await login('admin01'); await route('merchant','access'); assert.match(await content.innerText(), new RegExp(account));
- const employee = content.getByRole('row').filter({hasText:'staff01'});
- await employee.getByRole('button', { name: '停用账户', exact:true }).click(); await confirm();
- await employee.getByRole('button', { name: '启用账户', exact:true }).waitFor();
- await employee.getByRole('button', { name: '启用账户', exact:true }).click(); await confirm();
- await employee.getByRole('button', { name: '停用账户', exact:true }).waitFor();
- });
- await check('渠道配置读取与 HTTPS 地址校验', async () => {
- await route('merchant','channels'); await content.getByLabel('事件回调地址').fill('http://127.0.0.1/events'); if (!await content.getByRole('checkbox').isChecked()) await content.locator('.el-checkbox').click();
- const rejected = page.waitForResponse(response => response.request().method() === 'PUT' && response.url().endsWith('/channels'));
- await content.getByRole('button', { name: '保存接入配置' }).click(); assert.equal((await rejected).status(),422);
- await content.locator('.error-state').waitFor(); await content.getByRole('button',{name:'重新读取资料'}).click(); await waitLoaded();
- assert.match(await content.innerText(), /接入检查/);
- });
- await check('知识草稿真实保存与正文预览', async () => {
- await route('merchant','knowledge'); await content.getByRole('button', { name: '新建资料', exact: true }).click(); const title = `浏览器功能验收资料 ${Date.now()}`; await content.getByLabel('资料标题').fill(title); await content.getByLabel('文本内容').fill('键盘清洁前请断开电源。换货条件以当前有效售后规则为准。'); await content.getByRole('button', { name: '保存草稿', exact: true }).click(); await page.locator('.app-notice').filter({hasText:'知识草稿已保存'}).or(page.getByRole('alert')).waitFor(); assert.equal(await page.getByRole('alert').count(),0,await page.locator('body').innerText()); assert.match(await content.innerText(), new RegExp(title)); await content.getByRole('button', { name: '预览检索内容' }).click(); assert.match(await page.getByRole('dialog').innerText(), /键盘清洁前请断开电源/); await confirm();
- });
- await check('客服真实会话接管、回复与释放', async () => {
- // 仅创建会话,不发送客户生成请求;避免触发付费模型调用。
- const auth = await context.request.post(`${base}/openapi/v1/auth/sessions`, { data: { tenant_id: credentials.tenant_id, username: 'customer01', password: credentials.password } }); assert.equal(auth.status(), 200); const identity = await auth.json();
- const conversationResponse = await context.request.post(`${base}/openapi/v1/conversations`, { headers: { Authorization: `Bearer ${identity.access_token}`, 'Idempotency-Key': crypto.randomUUID() }, data: { store_id: credentials.store_id } }); assert.equal(conversationResponse.status(), 201); const conversation = await conversationResponse.json(); assert.equal(conversation.subject_id,credentials.actors.customer01); assert.equal(conversation.assigned_staff_id,null);
- const forbiddenOrders = await context.request.get(`${base}/openapi/v1/orders?store_id=${credentials.store_id}&subject_id=${credentials.actors.staff01}`,{headers:{Authorization:`Bearer ${identity.access_token}`}}); assert.equal(forbiddenOrders.status(),403);
- await login('staff01'); await route('staff','queue'); await content.getByRole('button').filter({ hasText: conversation.conversation_id }).click(); await waitLoaded(); await content.getByRole('button', { name: '接管会话', exact: true }).click(); await confirm();
- await content.getByRole('button',{name:'返回会话队列'}).click(); await waitLoaded(); await content.getByRole('button',{name:'我的会话',exact:true}).click(); await content.getByRole('button').filter({hasText:conversation.conversation_id}).click(); await waitLoaded();
- const filtered = page.waitForResponse(response=>response.request().method()==='GET' && response.url().includes('/openapi/v1/orders?') && response.url().includes(`subject_id=${credentials.actors.customer01}`)); await content.getByRole('button',{name:'选择授权订单'}).click(); assert.equal((await filtered).status(),200);
- const reply = `人工回复验收 ${Date.now()}`; await content.getByLabel('人工回复').fill(reply); await content.getByRole('button', { name: '发送回复', exact: true }).click(); await page.locator('.app-notice').filter({hasText:'回复已发送'}).waitFor(); assert.match(await content.innerText(), new RegExp(reply)); await content.getByRole('button', { name: '结束接管', exact: true }).click(); await confirm(); assert.equal(await content.getByRole('button', { name: '发送回复' }).isDisabled(), true);
- await context.request.delete(`${base}/openapi/v1/auth/sessions/current`, { headers: { Authorization: `Bearer ${identity.access_token}` } });
- });
- assert.deepEqual(errors, []);
- await mkdir(new URL('../../.work/frontend-verification/', import.meta.url), { recursive: true });
- await writeFile(new URL('../../.work/frontend-verification/merchant.json', import.meta.url), JSON.stringify({ date: new Date().toISOString(), pages: 32, checks: results, browserErrors: errors, generatedModelCalls: 0 }, null, 2));
- } finally { await browser.close(); }
|