// 独立 Chrome 核对预警页面;预警写入和附件上传使用受控接口,不修改真实业务数据。 const fs = require('node:fs/promises') const path = require('node:path') const assert = require('node:assert/strict') const { chromium } = require(process.env.SW_PLAYWRIGHT_MODULE || 'playwright') ;(async () => { const token = (await fs.readFile(path.join(process.env.TEMP, 'sw-kra-http-test-token'), 'utf8')).trim() const browser = await chromium.launch({ channel: 'chrome', headless: true }) const results = [] const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9Zl1sAAAAASUVORK5CYII=', 'base64') try { for (const level of [0, 1, 2, 3]) { const context = await browser.newContext({ viewport: { width: 1500, height: 1080 } }) await context.addInitScript(value => localStorage.setItem('token', value), token) const page = await context.newPage() const pageErrors = [] const writes = [] const queries = [] let uploads = 0 let unavailable = false const rows = [1, 2, 3, 4, 5].map(status => ({ id: String(100 + status), platformCode: 'lx', platformName: '龙翔', ruleName: `测试规则-${status}`, ruleType: '测试分类', targetPromoteName: `渠道-${status}`, businessName: '测试一级商务', alertContent: '测试预警内容', status, createdAt: '2026-11-03 10:20:30', updatedAt: '2026-11-03 10:20:30' })) const logs = new Map(rows.map(row => [row.id, []])) const log = (id, action, note, attachments = []) => logs.get(id).unshift({ id: String(writes.length), operatorName: '页面测试账号', logType: action ? 1 : 2, logTypeLabel: action ? '状态变更操作' : '添加独立备注', actionDetail: action || '', noteContent: note || '', attachments, createdAt: '2026-11-03 10:30:00' }) page.on('pageerror', error => pageErrors.push(error.message)) await page.route('http://127.0.0.1:8080/api/**', async route => { const request = route.request() const url = new URL(request.url()) const api = url.pathname.slice(4) const ok = data => route.fulfill({ json: { code: 0, msg: '成功', data } }) if (api === '/sw/alertRule/ruleTypes') return ok(['测试分类']) if (api === '/sw/alertRecord/list') { queries.push(url.search) let filtered = rows.filter(row => !url.searchParams.get('status') || row.status === Number(url.searchParams.get('status'))) if (url.searchParams.get('ruleName')) filtered = filtered.filter(row => row.ruleName.includes(url.searchParams.get('ruleName'))) return ok({ pageData: { rows: unavailable ? [] : filtered, total: unavailable ? 0 : filtered.length }, statusCount: unavailable ? {} : { 1: 1, 2: 1, 3: 1, 4: 1, 5: 1 }, available: !unavailable }) } if (api.startsWith('/sw/alertRecord/detail/')) { const id = api.split('/').at(-1) return ok({ alertRecord: rows.find(row => row.id === id), operationLogs: logs.get(id), canAudit: level === 1 }) } if (api === '/fileUploadAndDownload/upload') { uploads++ return ok({ file: { ID: 800 + uploads, url: `/files/verify-${uploads}.png`, name: 'test.png' } }) } if (api.startsWith('/sw/alertRecord/') && request.method() === 'POST') { const payload = request.postDataJSON() const id = String(payload.alertId) writes.push({ path: api, body: payload }) const action = api.split('/').at(-1) if (action === 'addNote') log(id, '', payload.noteContent, ['/files/verify-1.png']) else { rows.find(row => row.id === id).status = { startProcess: 2, submitAudit: 3, reject: 4, reProcess: 2, approve: 5 }[action] log(id, { startProcess: 'start', submitAudit: 'submit', reject: 'reject', reProcess: 'reprocess', approve: 'pass' }[action], payload.note) } return ok({}) } const response = await route.fetch({ url: 'http://127.0.0.1:18000' + api + url.search, headers: { ...request.headers(), 'x-token': token } }) if (api === '/user/getUserInfo') { const body = await response.json() Object.assign(body.data.userInfo, { ID: 70000 + level, level, nickName: '页面测试账号', phone: '', email: '', authorityId: level === 0 ? 888 : 900 + level }) return route.fulfill({ response, json: body }) } return route.fulfill({ response }) }) await page.route('**/files/verify-*.png', route => route.fulfill({ contentType: 'image/png', body: png })) await page.goto('http://127.0.0.1:8080/#/layout/sw-AlertCenter/AlertList') await page.getByText('测试规则-1', { exact: true }).waitFor() const result = { level, stateButtons: [], passed: true } const tableRow = status => page.locator('.el-table__body-wrapper tr').filter({ hasText: `测试规则-${status}` }) const dialog = page.getByRole('dialog', { name: '预警处理 / 详情', exact: true }) const close = async () => { await dialog.getByRole('button', { name: '关闭', exact: true }).click(); await dialog.waitFor({ state: 'hidden' }) } for (const status of [1, 2, 3, 4, 5]) { const expected = { 1: '处理', 2: '继续处理', 3: level === 1 ? '审核' : '查看', 4: '重新处理', 5: '查看' }[status] await tableRow(status).getByRole('button', { name: expected, exact: true }).click() await dialog.getByText('测试预警内容', { exact: true }).waitFor() assert.equal(await dialog.getByRole('button', { name: '审核通过', exact: true }).count(), status === 3 && level === 1 ? 1 : 0) assert.equal(await dialog.getByPlaceholder('填写处理备注...').count(), status === 5 || (status === 3 && level !== 1) ? 0 : 1) result.stateButtons.push({ status, label: expected }) await close() } if (level === 1) { await tableRow(1).getByRole('button', { name: '处理', exact: true }).click() await dialog.getByPlaceholder('填写处理备注...').waitFor() await dialog.locator('input[type="file"]').setInputFiles({ name: 'test.png', mimeType: 'image/png', buffer: png }) await dialog.locator('textarea').evaluate((element, bytes) => { const transfer = new DataTransfer() transfer.items.add(new File([new Uint8Array(bytes)], 'paste.png', { type: 'image/png' })) element.dispatchEvent(new ClipboardEvent('paste', { bubbles: true, clipboardData: transfer })) }, [...png]) await dialog.locator('.alert-upload-zone').evaluate((element, bytes) => { const transfer = new DataTransfer() transfer.items.add(new File([new Uint8Array(bytes)], 'drop.png', { type: 'image/png' })) element.dispatchEvent(new DragEvent('drop', { bubbles: true, dataTransfer: transfer })) }, [...png]) assert.equal(await dialog.locator('.pending-image').count(), 3) await dialog.locator('.pending-image').first().getByRole('button', { name: '移除', exact: true }).click() await dialog.locator('.pending-image').first().getByRole('button', { name: '移除', exact: true }).click() assert.equal(await dialog.locator('.pending-image').count(), 1) assert.equal(uploads, 0) await dialog.getByRole('button', { name: '添加备注', exact: true }).click() await page.getByText('请填写处理备注后再提交附件', { exact: true }).waitFor() assert.equal(uploads, 0) await dialog.getByPlaceholder('填写处理备注...').fill('测试备注和图片') await dialog.getByRole('button', { name: '开始处理', exact: true }).click() await page.getByText('请先点击「添加备注」上传图片,或移除暂存图片后再操作', { exact: true }).waitFor() assert.equal(writes.length, 0) await dialog.getByRole('button', { name: '添加备注', exact: true }).click() await dialog.getByText('备注:测试备注和图片', { exact: true }).waitFor() assert.equal(uploads, 1) assert.deepEqual(writes[0].body.attachmentIds, ['801']) for (const [button, next, confirmation] of [ ['开始处理', '提交审核', true], ['提交审核', '审核通过', true], ['审核驳回', '重新处理', true], ['重新处理', '提交审核', true], ['提交审核', '审核通过', true], ['审核通过', null, false] ]) { await dialog.getByRole('button', { name: button, exact: true }).click() if (confirmation) await page.getByRole('dialog', { name: '操作确认', exact: true }).getByRole('button', { name: '确定', exact: true }).click() if (next) await dialog.getByRole('button', { name: next, exact: true }).waitFor() else await dialog.getByPlaceholder('填写处理备注...').waitFor({ state: 'hidden' }) } assert.equal(writes.length, 7) await page.screenshot({ path: path.join(process.env.TEMP, 'sw-alert-record-workflow.png'), animations: 'disabled' }) await close() result.workflowAndAttachments = true result.pasteDropAndRemove = true } if (level === 2) { await page.getByRole('button', { name: '未处理 1', exact: true }).click() await tableRow(2).waitFor({ state: 'hidden' }) assert.ok(queries.at(-1).includes('status=1')) await page.getByRole('button', { name: '未处理 1', exact: true }).click() await tableRow(2).waitFor() assert.ok(!queries.at(-1).includes('status=1')) unavailable = true await page.locator('.kra-search-box').getByRole('button', { name: '搜索', exact: true }).click() await page.getByText('渠道数据尚未接入,暂时无法核对预警可见范围。', { exact: true }).waitFor() assert.equal(await page.getByText('未接入', { exact: true }).count(), 5) result.statusCardsAndUnavailable = true } assert.deepEqual(pageErrors, []) result.pageErrors = pageErrors results.push(result) console.log(JSON.stringify(result)) await context.close() } await fs.writeFile('docs/sw6-to-kra/alert-records-page-2026-11-03.json', JSON.stringify({ method: '真实 Chrome 渲染;受控身份、预警数据、写接口和图片上传;未修改真实业务数据', results }, null, 2) + '\n') } finally { await browser.close() } })().catch(error => { console.error(error); process.exitCode = 1 })