feat(需求): ①全站分页pageSize统一默认10+10/20/50/100选项(9页默认值15/20→10+6个兜底常量统一+撞款台账升级OldPagination+商品类目树顶层标准分页) ②修复6页OldPagination缺import分页器不渲染(不符合ASIN/查询ASIN/最低价ASIN/生成记录/店铺密钥/店铺管理)+查询ASIN操作列按钮间距+最低价范围筛选number类型报错 ③e2e断言对齐现状(顶栏h1/移除收起按钮后规格同步)+国家显示中文断言固化
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
// 样办校验:生成记录页旧版样式落地实测(本地真实栈)。
|
||||
import { chromium } from '@playwright/test'
|
||||
const BASE = 'http://localhost:5174/admin-vue'
|
||||
const browser = await chromium.launch({ channel: 'msedge', headless: true })
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } })
|
||||
await page.goto(`${BASE}/`, { waitUntil: 'networkidle' })
|
||||
await page.waitForTimeout(500)
|
||||
if (await page.locator('#loginUsername').isVisible().catch(() => false)) {
|
||||
await page.locator('#loginUsername').fill('admin')
|
||||
await page.locator('#loginPassword').fill('admin123')
|
||||
await page.locator('.btn-login').click()
|
||||
await page.waitForURL(/admin-vue\/(account|asin|shop|records)/, { timeout: 20000 }).catch(() => {})
|
||||
await page.waitForTimeout(1200)
|
||||
}
|
||||
await page.goto(`${BASE}/records/history`, { waitUntil: 'domcontentloaded' })
|
||||
await page.locator('.form-box').first().waitFor({ state: 'visible', timeout: 15000 })
|
||||
await page.waitForLoadState('networkidle').catch(() => {})
|
||||
await page.waitForTimeout(600)
|
||||
const out = await page.evaluate(() => {
|
||||
const cs = (el, prop) => (el ? getComputedStyle(el)[prop] : null)
|
||||
const fb = document.querySelector('.form-box')
|
||||
const pb = document.querySelector('.panel-box')
|
||||
const th = document.querySelector('.table-scroll th')
|
||||
const td = document.querySelector('.table-scroll td')
|
||||
const btn = document.querySelector('.form-box .btn')
|
||||
const inp = document.querySelector('.form-group input, .form-group select')
|
||||
return {
|
||||
formBox: { bg: cs(fb, 'backgroundColor'), radius: cs(fb, 'borderRadius'), border: cs(fb, 'borderTopColor') },
|
||||
panelBox: { bg: cs(pb, 'backgroundColor') },
|
||||
h3: cs(document.querySelector('.panel-box h3'), 'fontSize'),
|
||||
input: { minH: inp && cs(inp, 'minHeight'), bg: inp && cs(inp, 'backgroundColor'), radius: inp && cs(inp, 'borderRadius') },
|
||||
btn: { minH: cs(btn, 'minHeight'), bgImage: btn && getComputedStyle(btn).backgroundImage.slice(0, 60), radius: cs(btn, 'borderRadius'), color: cs(btn, 'color') },
|
||||
th: { bg: cs(th, 'backgroundColor'), color: cs(th, 'color'), fontSize: cs(th, 'fontSize'), weight: cs(th, 'fontWeight') },
|
||||
td: { color: td && cs(td, 'color'), fontSize: td && cs(td, 'fontSize') },
|
||||
tableScroll: { border: cs(document.querySelector('.table-scroll'), 'borderTopColor'), radius: cs(document.querySelector('.table-scroll'), 'borderRadius') },
|
||||
rowCount: document.querySelectorAll('.table-scroll tbody tr').length,
|
||||
thumbb: !!document.querySelector('.thumb'),
|
||||
pagination: (document.querySelector('.pagination')?.textContent || '').trim().slice(0, 60),
|
||||
modal: document.querySelector('.modal-mask') === null,
|
||||
}
|
||||
})
|
||||
console.log(JSON.stringify(out, null, 2))
|
||||
await browser.close()
|
||||
@@ -0,0 +1,96 @@
|
||||
// round2 DOM 审计:登录本地真实栈后,对 15 页提取结构化观感事实(颜色/卡壳/分页坐标/按钮形态/标题/列头/空态)。
|
||||
// 用法:cd admin-frontend-vue && node scripts/round2-audit.mjs
|
||||
import { chromium } from '@playwright/test'
|
||||
|
||||
const BASE = process.env.BASE_URL || 'http://localhost:5174/admin-vue'
|
||||
|
||||
const PAGES = [
|
||||
['account-users', 'account/users'],
|
||||
['account-menus', 'account/menus'],
|
||||
['account-groups', 'account/groups'],
|
||||
['asin-registry', 'asin-center/registry'],
|
||||
['asin-invalid', 'asin-center/invalid'],
|
||||
['asin-query', 'asin-center/query'],
|
||||
['asin-categories', 'asin-center/categories'],
|
||||
['asin-skip-price', 'asin-center/skip-price'],
|
||||
['shop-keys', 'shop-center/keys'],
|
||||
['shop-shops', 'shop-center/shops'],
|
||||
['shop-data-tasks', 'shop-center/data-tasks'],
|
||||
['records-history', 'records/history'],
|
||||
['records-software-version', 'records/software-version'],
|
||||
['records-digital-human-version', 'records/digital-human-version'],
|
||||
['records-image-video-tasks', 'records/image-video-tasks'],
|
||||
]
|
||||
|
||||
const browser = await chromium.launch({ channel: 'msedge', headless: true })
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, locale: 'zh-CN' })
|
||||
const page = await context.newPage()
|
||||
|
||||
async function ensureLoggedIn() {
|
||||
await page.goto(`${BASE}/`, { waitUntil: 'networkidle' })
|
||||
await page.waitForTimeout(600)
|
||||
if (await page.locator('#loginUsername').isVisible().catch(() => false)) {
|
||||
await page.locator('#loginUsername').fill('admin')
|
||||
await page.locator('#loginPassword').fill('admin123')
|
||||
await page.locator('.btn-login').click()
|
||||
await page.waitForURL(/admin-vue\/(account|asin|shop|records)/, { timeout: 20000 }).catch(() => {})
|
||||
await page.waitForTimeout(1200)
|
||||
}
|
||||
}
|
||||
await ensureLoggedIn()
|
||||
|
||||
const out = {}
|
||||
for (const [name, path] of PAGES) {
|
||||
await page.goto(`${BASE}/${path}`, { waitUntil: 'domcontentloaded' })
|
||||
await page.locator('.page-heading').first().waitFor({ state: 'visible', timeout: 12000 }).catch(() => {})
|
||||
await page.waitForLoadState('networkidle').catch(() => {})
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
const fact = await page.evaluate(() => {
|
||||
const cs = (el, prop) => (el ? getComputedStyle(el)[prop] : null)
|
||||
const root = getComputedStyle(document.documentElement)
|
||||
const q = (sel) => document.querySelectorAll(sel)
|
||||
const text = (el) => (el ? (el.textContent || '').trim() : '')
|
||||
const heading = document.querySelector('.page-heading h1, .page-heading h2, .page-heading .page-title')
|
||||
const headingText = heading ? (heading.textContent || '').trim() : null
|
||||
const pags = [...q('.el-pagination')].map((p) => {
|
||||
const r = p.getBoundingClientRect()
|
||||
return { y: Math.round(r.top), x: Math.round(r.left), w: Math.round(r.width) }
|
||||
})
|
||||
const btns = [...q('.el-button')].map((b) => {
|
||||
const t = (b.textContent || '').trim().slice(0, 8)
|
||||
return { t, link: b.classList.contains('is-link'), text: b.classList.contains('is-text'), primary: b.classList.contains('el-button--primary'), danger: b.classList.contains('el-button--danger') }
|
||||
})
|
||||
const ths = [...q('.el-table__header th')].map((x) => (x.textContent || '').trim())
|
||||
const empty = q('.el-empty, .empty-tip, .empty').length
|
||||
const emptyText = text(document.querySelector('.el-empty, .empty-tip')) || null
|
||||
const indigoEls = [...q('body *')].filter((el) => {
|
||||
const b = cs(el, 'backgroundColor')
|
||||
const c = cs(el, 'color')
|
||||
return /rgb\(99,\s*102,\s*241\)|rgb\(238,\s*240,\s*254\)|#6366f1|#eef0fe/i.test(`${b} ${c}`)
|
||||
}).slice(0, 4).map((el) => ({ tag: el.tagName, cls: (el.className && String(el.className).slice(0, 60)) || '', bg: cs(el, 'backgroundColor'), color: cs(el, 'color') }))
|
||||
const cards = q('.el-card').length
|
||||
const toolbars = q('.table-toolbar, .list-toolbar').length
|
||||
const pageHeadingDesc = text(document.querySelector('.page-heading p'))
|
||||
return {
|
||||
heading: headingText, desc: (pageHeadingDesc || '').slice(0, 80),
|
||||
primary: root.getPropertyValue('--el-color-primary').trim(),
|
||||
bg: root.getPropertyValue('--el-bg-color').trim() || cs(document.body, 'backgroundColor'),
|
||||
paginations: pags, cards, toolbars,
|
||||
ths: ths.slice(0, 14), empty, emptyText,
|
||||
buttonShape: {
|
||||
total: btns.length,
|
||||
link: btns.filter((b) => b.link).length,
|
||||
textPlain: btns.filter((b) => b.text && !b.link).length,
|
||||
primarySolid: btns.filter((b) => b.primary && !b.text && !b.link).length,
|
||||
danger: btns.filter((b) => b.danger).length,
|
||||
sample: btns.slice(0, 12),
|
||||
},
|
||||
indigoEls,
|
||||
}
|
||||
})
|
||||
out[name] = { path, ...fact }
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(out, null, 2))
|
||||
await browser.close()
|
||||
@@ -0,0 +1,97 @@
|
||||
// round2 现状截图基线:本地真实栈(5174 -> 18080 Java),登录后逐页截图 + 收集 pageerror/console error。
|
||||
// 用法:cd admin-frontend-vue && node scripts/round2-shots.mjs
|
||||
import { chromium } from '@playwright/test'
|
||||
import { mkdirSync } from 'node:fs'
|
||||
|
||||
const BASE = process.env.BASE_URL || 'http://localhost:5174/admin-vue'
|
||||
const OUT = 'test-results/round2-baseline'
|
||||
mkdirSync(OUT, { recursive: true })
|
||||
|
||||
const PAGES = [
|
||||
['account-users', 'account/users'],
|
||||
['account-menus', 'account/menus'],
|
||||
['account-groups', 'account/groups'],
|
||||
['asin-registry', 'asin-center/registry'],
|
||||
['asin-invalid', 'asin-center/invalid'],
|
||||
['asin-query', 'asin-center/query'],
|
||||
['asin-categories', 'asin-center/categories'],
|
||||
['asin-skip-price', 'asin-center/skip-price'],
|
||||
['shop-keys', 'shop-center/keys'],
|
||||
['shop-shops', 'shop-center/shops'],
|
||||
['shop-data-tasks', 'shop-center/data-tasks'],
|
||||
['records-history', 'records/history'],
|
||||
['records-software-version', 'records/software-version'],
|
||||
['records-digital-human-version', 'records/digital-human-version'],
|
||||
['records-image-video-tasks', 'records/image-video-tasks'],
|
||||
]
|
||||
|
||||
const browser = await chromium.launch({ channel: 'msedge', headless: true })
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, locale: 'zh-CN' })
|
||||
const page = await context.newPage()
|
||||
|
||||
const problems = new Map() // page -> string[]
|
||||
page.on('pageerror', (e) => {
|
||||
const key = page.url()
|
||||
const list = problems.get(key) || []
|
||||
list.push(`pageerror: ${e.message}`)
|
||||
problems.set(key, list)
|
||||
})
|
||||
page.on('console', (m) => {
|
||||
if (m.type() === 'error') {
|
||||
const key = page.url()
|
||||
const list = problems.get(key) || []
|
||||
list.push(`console.error: ${m.text().slice(0, 300)}`)
|
||||
problems.set(key, list)
|
||||
}
|
||||
})
|
||||
|
||||
// 登录(真实 UI 表单)
|
||||
async function ensureLoggedIn() {
|
||||
await page.goto(`${BASE}/`, { waitUntil: 'networkidle' })
|
||||
await page.waitForTimeout(600)
|
||||
const loginVisible = await page.locator('#loginUsername').isVisible().catch(() => false)
|
||||
if (loginVisible) {
|
||||
await page.locator('#loginUsername').fill('admin')
|
||||
await page.locator('#loginPassword').fill('admin123')
|
||||
await page.locator('.btn-login').click()
|
||||
// 等离开登录页(进入后台)
|
||||
await page.waitForURL(/admin-vue\/(account|asin|shop|records)/, { timeout: 20000 }).catch(() => {})
|
||||
await page.waitForTimeout(1200)
|
||||
}
|
||||
}
|
||||
|
||||
await ensureLoggedIn()
|
||||
console.log('logged-in url:', page.url())
|
||||
|
||||
const report = []
|
||||
for (const [name, path] of PAGES) {
|
||||
const url = `${BASE}/${path}`
|
||||
try {
|
||||
await page.goto(url, { waitUntil: 'domcontentloaded' })
|
||||
// 等页头或内容出现(不同页标题不同,抓第一个标题/内容容器)
|
||||
await page.locator('.page-heading').first().waitFor({ state: 'visible', timeout: 12000 }).catch(() => {})
|
||||
await page.waitForLoadState('networkidle').catch(() => {})
|
||||
await page.waitForTimeout(500)
|
||||
await page.screenshot({ path: `${OUT}/${name}.jpg`, type: 'jpeg', quality: 82 })
|
||||
report.push(`ok ${name} ${url}`)
|
||||
} catch (e) {
|
||||
report.push(`FAIL ${name} ${url} :: ${String(e).slice(0, 200)}`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== screenshots ===')
|
||||
console.log(report.join('\n'))
|
||||
|
||||
console.log('\n=== page errors (运行时异常,供排查) ===')
|
||||
let anyError = false
|
||||
for (const [url, list] of problems) {
|
||||
const clean = url.replace(BASE, '')
|
||||
if (list.length) {
|
||||
anyError = true
|
||||
console.log(`-- ${clean}`)
|
||||
for (const l of [...new Set(list)]) console.log(' ', l)
|
||||
}
|
||||
}
|
||||
if (!anyError) console.log('(无 pageerror/console.error)')
|
||||
|
||||
await browser.close()
|
||||
@@ -0,0 +1,45 @@
|
||||
// round2 单批验证:guides 渲染 + 类目色点 token + 行内按钮实心(本地真实栈)。
|
||||
import { chromium } from '@playwright/test'
|
||||
const BASE = 'http://localhost:5174/admin-vue'
|
||||
const browser = await chromium.launch({ channel: 'msedge', headless: true })
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } })
|
||||
await page.goto(`${BASE}/`, { waitUntil: 'networkidle' })
|
||||
await page.waitForTimeout(500)
|
||||
if (await page.locator('#loginUsername').isVisible().catch(() => false)) {
|
||||
await page.locator('#loginUsername').fill('admin')
|
||||
await page.locator('#loginPassword').fill('admin123')
|
||||
await page.locator('.btn-login').click()
|
||||
await page.waitForURL(/admin-vue\/(account|asin|shop|records)/, { timeout: 20000 }).catch(() => {})
|
||||
await page.waitForTimeout(1200)
|
||||
}
|
||||
async function open(path) {
|
||||
await page.goto(`${BASE}/${path}`, { waitUntil: 'domcontentloaded' })
|
||||
await page.locator('.page-heading').first().waitFor({ state: 'visible', timeout: 15000 }).catch(() => {})
|
||||
await page.waitForLoadState('networkidle').catch(() => {})
|
||||
await page.waitForTimeout(600)
|
||||
}
|
||||
|
||||
await open('account/users')
|
||||
const guide = await page.locator('.operation-guide').count()
|
||||
const guideText = guide ? (await page.locator('.og-text').textContent())?.trim() : null
|
||||
console.log('users guide visible:', guide > 0, '| text:', guideText)
|
||||
|
||||
await open('asin-center/categories')
|
||||
const token = await page.evaluate(() => {
|
||||
const el = document.querySelector('.tree-node-mark')
|
||||
if (!el) return null
|
||||
const cs = getComputedStyle(el)
|
||||
return { color: cs.color, bg: cs.backgroundColor }
|
||||
})
|
||||
console.log('categories tree-node-mark:', JSON.stringify(token))
|
||||
|
||||
await open('asin-center/registry')
|
||||
const solid = await page.evaluate(() => {
|
||||
const btns = [...document.querySelectorAll('.el-table .el-button--small')]
|
||||
const linkOrText = btns.filter((b) => b.classList.contains('is-link') || b.classList.contains('is-text')).length
|
||||
const primary = btns.filter((b) => b.classList.contains('el-button--primary')).length
|
||||
return { smallSolid: btns.length, linkOrText, primary }
|
||||
})
|
||||
console.log('registry row buttons:', JSON.stringify(solid))
|
||||
|
||||
await browser.close()
|
||||
Reference in New Issue
Block a user