Files
kra-new/scripts/sw_dashboard_layout_smoke.cjs
T

219 lines
14 KiB
JavaScript

// 真实开发服务:检查 OA 数据、统计口径持久化与响应式布局。
// 临时修改当前测试账号的统计口径,结束时恢复;截图只写入指定的私有目录。
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 base = process.env.SW_UI_BASE_URL || 'http://127.0.0.1:8080'
const output = process.env.SW_UI_OUTPUT_DIR
assert.ok(output && process.env.SW_TEST_TOKEN_FILE, 'Set SW_UI_OUTPUT_DIR and SW_TEST_TOKEN_FILE')
const token = (await fs.readFile(process.env.SW_TEST_TOKEN_FILE, 'utf8')).trim()
const browser = await chromium.launch({ channel: 'chrome', headless: true })
const context = await browser.newContext({ viewport: { width: 1600, height: 1000 } })
const headers = { 'x-token': token }
const api = async (method, endpoint, data) => {
const response = await context.request.fetch(`${base}/api${endpoint}`, { method, headers, data })
assert.ok(response.ok())
const body = await response.json()
assert.equal(body.code, 0)
return body.data
}
let original
const report = { checks: [], pageErrors: [] }
try {
await fs.mkdir(output, { recursive: true })
original = await api('GET', '/sw/user/preference')
await fs.writeFile(path.join(output, 'original-preference.json'), JSON.stringify(original))
await context.addInitScript(value => localStorage.setItem('token', value), token)
const page = await context.newPage()
page.on('pageerror', e => report.pageErrors.push(e.message))
const activationPath = '/sw/promoteActivePool/bindList'
await page.goto(`${base}/#/layout/sw-BusMgmt/ChannelActivationList`)
await page.locator('.el-table__body-wrapper tr').first().waitFor()
const activation = await api('GET', `${activationPath}?pageNum=1&pageSize=10&platformNames=lx,hw,yq`)
assert.equal(activation.available, true)
assert.ok(activation.total > 0)
assert.equal(await page.locator('.el-table__body-wrapper tr').count(), activation.rows.length)
assert.equal(await page.locator('.el-alert--warning').count(), 0)
report.checks.push({ name: 'actual OA activation rows', total: activation.total })
await page.screenshot({ path: path.join(output, 'activation.png'), animations: 'disabled' })
const pagination = page.locator('.kra-pagination')
const pageRequests = []
const trackPage = request => {
if (request.url().includes(activationPath)) pageRequests.push(Number(new URL(request.url()).searchParams.get('pageNum')))
}
page.on('request', trackPage)
const movePage = async (target, action) => {
pageRequests.length = 0
const [response] = await Promise.all([
page.waitForResponse(r => r.url().includes(activationPath) && Number(new URL(r.url()).searchParams.get('pageNum')) === target),
action()
])
const expected = (await response.json()).data
await page.waitForFunction(() => !document.querySelector('.el-loading-mask'))
assert.equal((await pagination.locator('.el-pager .is-active').innerText()).trim(), String(target))
assert.equal(await page.locator('.el-table__body-wrapper tr').count(), expected.rows.length)
const names = await page.locator('.el-table__body-wrapper tr td:nth-child(2) .cell').allTextContents()
assert.deepEqual(names.map(name => name.trim()), expected.rows.map(row => row.promoteAccount))
assert.deepEqual(pageRequests, [target], 'pagination must not trigger a second request back to page 1')
}
assert.ok(activation.total > 10, 'Pagination check needs more than one page')
await movePage(2, () => pagination.locator('.btn-next').click())
const lastPage = Math.ceil(activation.total / 10)
if (lastPage > 2) await movePage(lastPage, () => pagination.locator('.el-pager .number').filter({ hasText: new RegExp(`^${lastPage}$`) }).click())
await page.screenshot({ path: path.join(output, 'activation-last-page.png'), animations: 'disabled' })
await movePage(lastPage - 1, () => pagination.locator('.btn-prev').click())
if (lastPage - 1 !== 1) await movePage(1, () => pagination.locator('.el-pager .number').filter({ hasText: /^\s*1\s*$/ }).click())
page.off('request', trackPage)
report.checks.push({ name: 'activation pagination: next, last, previous, first; response rows match and no page reset', total: activation.total, lastPage })
const preference = page.getByRole('group', { name: '统计口径', exact: true })
for (const [key, label] of [['includeSubPromote', '包含下级渠道'], ['idcardDedup', '身份证去重']]) {
const checkbox = preference.getByRole('checkbox', { name: label, exact: true })
await page.waitForFunction(label => [...document.querySelectorAll('.business-preferences input')].some(el => el.closest('label')?.textContent.includes(label) && !el.disabled), label)
assert.equal(await checkbox.isChecked(), original[key])
const [saved] = await Promise.all([
page.waitForResponse(r => r.url().includes('/sw/user/preference') && r.request().method() === 'PUT'),
page.waitForResponse(r => r.url().includes(activationPath)),
preference.getByText(label, { exact: true }).click()
])
assert.equal((await saved.json()).code, 0)
assert.equal((await api('GET', '/sw/user/preference'))[key], !original[key])
await page.reload()
await preference.waitFor()
await page.waitForFunction(() => [...document.querySelectorAll('.business-preferences input')].every(el => !el.disabled))
assert.equal(await preference.getByRole('checkbox', { name: label, exact: true }).isChecked(), !original[key])
report.checks.push({ name: `${key}: visible, persisted, page refreshed` })
}
await api('PUT', '/sw/user/preference', original)
// 保存失败不能让勾选状态与服务端失去同步。
await page.reload()
await page.waitForFunction(() => document.querySelectorAll('.business-preferences input').length === 2 && [...document.querySelectorAll('.business-preferences input')].every(el => !el.disabled))
const rejectSave = route => route.request().method() === 'PUT'
? route.fulfill({ json: { code: 7, msg: '测试保存失败', data: {} } }) : route.continue()
await page.route('**/api/sw/user/preference', rejectSave)
await Promise.all([
page.waitForResponse(r => r.url().includes('/sw/user/preference') && r.request().method() === 'PUT'),
preference.getByText('包含下级渠道', { exact: true }).click()
])
await page.waitForFunction(() => [...document.querySelectorAll('.business-preferences input')].every(el => !el.disabled))
assert.equal(await preference.getByRole('checkbox', { name: '包含下级渠道', exact: true }).isChecked(), original.includeSubPromote)
await page.unroute('**/api/sw/user/preference', rejectSave)
report.checks.push({ name: 'failed preference save preserves previous value' })
const search = () => page.locator('.kra-search-box').getByRole('button', { name: '查询', exact: true }).click()
const activationPattern = '**/api/sw/promoteActivePool/bindList?*'
await page.route(activationPattern, route => route.fulfill({ json: { code: 7, msg: '测试查询失败', data: {} } }))
await search()
await page.getByText('查询失败,请重试', { exact: true }).waitFor()
assert.equal(await page.locator('.el-table__body-wrapper tr').count(), 0)
await page.unroute(activationPattern)
await page.route(activationPattern, route => route.fulfill({ json: { code: 0, data: { available: false, rows: [], total: 0 } } }))
await search()
await page.getByText('OA 数据源暂不可用', { exact: true }).waitFor()
await page.unroute(activationPattern)
await search()
await page.locator('.el-table__body-wrapper tr').first().waitFor()
report.checks.push({ name: 'activation error and unavailable states differ from empty results; retry recovers' })
for (const [name, route, grid] of [
['ranking', 'sw-DataAnalysis/RankingAnalysis', '.ranking-panels'],
['dashboard', 'dashboard', '.business-ranks']
]) {
await page.goto(`${base}/#/layout/${route}`)
await page.locator(grid).waitFor()
await page.waitForFunction(() => !document.querySelector('.el-loading-mask'))
await page.locator('.el-message').evaluateAll(nodes => nodes.forEach(node => node.querySelector('.el-message__closeBtn')?.click()))
if (name === 'ranking') {
for (const panel of await page.locator('.ranking-panel').all()) {
assert.match(await panel.innerText(), /上期环比/)
assert.doesNotMatch(await panel.innerText(), /百分点|\bpp\b/)
}
} else {
assert.equal(await page.getByText('快捷功能', { exact: true }).count(), 0)
assert.equal(await page.getByText('公告', { exact: true }).count(), 0)
assert.equal(await page.getByText('商务数据', { exact: true }).count(), 0)
assert.ok(await page.getByText('数据平台', { exact: true }).isVisible())
for (const index of [0, 1]) {
const panel = page.locator('.business-ranks > .el-card').nth(index)
await panel.locator('.business-rank-period').click()
const optionsId = await panel.getByRole('combobox').getAttribute('aria-controls')
await Promise.all([
page.waitForResponse(r => /\/sw\/workbench\/(registerTop|payRank)/.test(r.url()) && r.url().includes('period=last7')),
page.locator(`[id="${optionsId}"]`).getByRole('option', { name: '近7天', exact: true }).click()
])
await panel.locator('.ranking-table tbody tr').first().waitFor()
assert.equal(await panel.locator('.ranking-empty').count(), 0)
}
report.checks.push({ name: 'dashboard period filters refresh real data in compact tables' })
}
for (const [width, columns] of [[1920, 4], [1600, 4], [1366, 4], [1024, 2], [390, 1]]) {
await page.setViewportSize({ width, height: 1000 })
if (width === 390) await page.getByRole('button', { name: '打开菜单', exact: true }).waitFor()
await page.waitForFunction(({grid, columns}) => getComputedStyle(document.querySelector(grid)).gridTemplateColumns.split(' ').length === columns, { grid, columns })
const boxes = await page.locator(`${grid} > *`).evaluateAll(nodes => nodes.map(n => { const r = n.getBoundingClientRect(); return { x: r.x, y: r.y, right: r.right } }))
if (columns === 4) assert.ok(boxes.every(box => Math.abs(box.y - boxes[0].y) < 1))
assert.ok(boxes.every(box => box.x >= 0 && box.right <= width + 1), `panel overflow: ${name}, ${width}`)
const overflow = await page.locator(`${grid} .el-table__body-wrapper, ${grid} .ranking-table-wrap`).evaluateAll(nodes => nodes.some(n => n.querySelector('table')?.clientWidth > n.clientWidth + 1))
assert.equal(overflow, false, `table horizontal overflow: ${name}, ${width}`)
if (name === 'ranking') {
const tallRows = await page.locator('.ranking-table tr').evaluateAll(nodes => nodes.some(n => n.getBoundingClientRect().height > 40))
assert.equal(tallRows, false, `ranking rows should stay compact: ${width}`)
}
const clippedNumbers = await page.locator(`${grid} tbody .ranking-numeric, ${grid} .ranking-number`).evaluateAll(nodes => nodes.some(n => n.scrollWidth > n.clientWidth + 1))
assert.equal(clippedNumbers, false, `numeric values must be fully visible: ${name}, ${width}`)
const prefBox = await preference.boundingBox()
assert.ok(prefBox && prefBox.x >= 0 && prefBox.x + prefBox.width <= width, `preference overflow at ${width}`)
const toolbarOverflow = await page.getByRole('button', { name: '打开菜单', exact: true }).count() ? await page.locator('.business-preferences').evaluate(el => {
const header = el.closest('.fixed')
return header.scrollWidth > header.clientWidth + 1
}) : false
assert.equal(toolbarOverflow, false, `mobile header overflow: ${width}`)
await page.screenshot({ path: path.join(output, `${name}-${width}.png`), animations: 'disabled' })
report.checks.push({ name: `${name} layout`, width, columns, noOverflow: true })
}
await page.setViewportSize({ width: 1600, height: 1000 })
}
await page.goto(`${base}/#/layout/sw-BusMgmt/BusinessList`)
await page.getByRole('button', { name: '添加子商务', exact: true }).waitFor()
assert.equal(await page.locator('.business-view-switch').count(), 0)
assert.equal(await page.getByRole('tab', { name: '商务列表', exact: true }).count(), 0)
await page.screenshot({ path: path.join(output, 'business-list-admin.png'), animations: 'disabled' })
// 只替换展示身份核对三级专属切换器;读取接口仍用授权测试身份,不做业务写入。
await page.route('**/api/user/getUserInfo', async route => {
const response = await route.fetch()
const body = await response.json()
Object.assign(body.data.userInfo, { level: 3, authorityId: 903 })
await route.fulfill({ response, json: body })
})
await page.reload()
await page.getByRole('radiogroup', { name: '商务管理视图', exact: true }).waitFor()
assert.equal(await page.getByRole('button', { name: '添加子商务', exact: true }).count(), 0)
const viewSwitch = page.getByRole('radiogroup', { name: '商务管理视图', exact: true })
await Promise.all([
page.waitForResponse(r => r.url().includes('/sw/business/bindPlatform')),
viewSwitch.getByText('绑定列表', { exact: true }).click()
])
assert.equal(await page.getByPlaceholder('请输入账号', { exact: true }).isVisible(), false)
await viewSwitch.getByText('商务列表', { exact: true }).click()
assert.ok(await page.getByPlaceholder('请输入账号', { exact: true }).isVisible())
await page.screenshot({ path: path.join(output, 'business-list-level3.png'), animations: 'disabled' })
report.checks.push({ name: 'business list: no redundant single tab; controlled level-3 switch works' })
assert.deepEqual(report.pageErrors, [])
report.passed = true
} catch (e) {
const page = context.pages()[0]
if (page) await page.screenshot({ path: path.join(output, 'failure.png'), animations: 'disabled' }).catch(() => {})
throw e
} finally {
if (original) await api('PUT', '/sw/user/preference', original)
await fs.writeFile(path.join(output, 'ui-checks.json'), JSON.stringify(report, null, 2) + '\n')
await browser.close()
}
console.log(JSON.stringify(report))
})().catch(e => { console.error(e.stack); process.exitCode = 1 })