mach-cms的前台,用vue写的
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

259 lines
12 KiB

const { chromium } = require('playwright')
const BASE = 'http://localhost:5173'
const USER = 'ui_test'
const PASS = 'UiTest@123'
const results = []
let passed = 0
let failed = 0
function report(id, name, ok, detail = '') {
results.push({ id, name, ok, detail })
if (ok) passed++
else failed++
}
async function login(page) {
await page.goto(BASE + '/login')
await page.waitForSelector('#username', { timeout: 10000 })
await page.fill('#username', USER)
await page.fill('#password', PASS)
await page.click('button[type=submit]')
await page.waitForURL('**/admin/**', { timeout: 10000 })
}
async function step(browser, id, name, fn) {
const page = await browser.newPage()
page.setDefaultTimeout(10000)
page.on('pageerror', (e) => console.log(`[${id} pageerror]`, e.message))
try {
await fn(page)
report(id, name, true)
} catch (e) {
report(id, name, false, e.message.split('\n')[0])
} finally {
await page.close()
}
}
;(async () => {
const browser = await chromium.launch()
await step(browser, '2.5.1', '后台布局: 侧边栏+顶部栏', async (page) => {
await login(page)
await page.waitForSelector('.sidebar')
await page.waitForSelector('.topbar')
const nav = await page.locator('.sidebar-nav').textContent()
if (!nav.includes('文章管理') || !nav.includes('标签管理')) throw new Error('侧边栏缺少菜单')
})
await step(browser, '2.5.2', '文章列表表格', async (page) => {
await login(page)
await page.waitForSelector('.article-table')
const headers = await page.locator('.article-table th').allTextContents()
if (!headers.some((h) => h.includes('标题'))) throw new Error('缺标题列')
if (!headers.some((h) => h.includes('状态'))) throw new Error('缺状态列')
if (!headers.some((h) => h.includes('操作'))) throw new Error('缺操作列')
const rows = await page.locator('.article-table tbody tr').count()
if (rows < 1) throw new Error('无文章行')
})
await step(browser, '2.5.3', '状态标签颜色', async (page) => {
await login(page)
await page.waitForSelector('.article-table')
const badges = await page.evaluate(() => {
const arr = []
document.querySelectorAll('.status-badge').forEach((el) => {
arr.push({ text: el.textContent, cls: el.className })
})
return arr
})
if (badges.length < 1) throw new Error('无状态徽章')
for (const b of badges) {
const t = b.text
if (t === '草稿' && !b.cls.includes('draft')) throw new Error('草稿样式错误')
if (t === '已发布' && !b.cls.includes('published')) throw new Error('已发布样式错误')
if (t === '已归档' && !b.cls.includes('archived')) throw new Error('已归档样式错误')
}
})
await step(browser, '2.5.4', '创建文章', async (page) => {
await login(page)
await page.waitForSelector('.article-table')
const unique = '验收新建文章-' + Date.now()
await page.click('text=新建文章')
await page.waitForSelector('#title')
await page.fill('#title', unique)
await page.fill('#content', '这是通过 UI 创建的内容。')
await page.click('button[type=submit]')
await page.waitForURL('**/admin/articles')
await page.waitForTimeout(500)
await page.selectOption('.filter-select', { label: '草稿' })
await page.waitForTimeout(1000)
const body = await page.locator('.article-table').textContent()
if (!body.includes(unique)) throw new Error('列表未出现新文章')
})
await step(browser, '2.5.5', '编辑文章(预填充+更新)', async (page) => {
await login(page)
await page.waitForSelector('.article-table')
const firstTitle = (await page.locator('.article-table tbody tr td').first().textContent()).trim()
await page.locator('.article-table tbody tr').first().locator('button', { hasText: '编辑' }).click()
await page.waitForSelector('#title')
const prefill = await page.locator('#title').inputValue()
if (prefill !== firstTitle) throw new Error('预填充标题不一致: ' + prefill + ' vs ' + firstTitle)
const edited = prefill + '-编辑' + Date.now()
await page.fill('#title', edited)
await page.click('button[type=submit]')
await page.waitForURL('**/admin/articles')
await page.waitForTimeout(800)
const body = await page.locator('.article-table').textContent()
if (!body.includes(edited)) throw new Error('更新后标题未出现在列表')
})
await step(browser, '2.5.6', '发布文章', async (page) => {
await login(page)
await page.waitForSelector('.filter-select')
await page.selectOption('.filter-select', { label: '草稿' })
await page.waitForTimeout(1000)
const hasDraft = await page.locator('.article-table tbody tr', { hasText: '草稿' }).count()
if (hasDraft < 1) throw new Error('无草稿文章可发布')
const row = page.locator('.article-table tbody tr', { hasText: '草稿' }).first()
const title = (await row.locator('td').first().textContent()).trim()
await row.locator('button', { hasText: '发布' }).click()
await page.waitForTimeout(1500)
const updated = await page.evaluate(async (t) => {
const r = await fetch('/api/articles?page=1&size=50&status=PUBLISHED')
const j = await r.json()
const hit = j.data.list.find((a) => a.title === t)
return hit ? hit.status : null
}, title)
if (updated !== 'PUBLISHED') throw new Error('发布后状态=' + updated)
})
await step(browser, '2.5.7', '归档文章', async (page) => {
await login(page)
await page.waitForSelector('.article-table')
const hasPub = await page.locator('.article-table tbody tr', { hasText: '已发布' }).count()
if (hasPub < 1) throw new Error('无已发布文章可归档')
const row = page.locator('.article-table tbody tr', { hasText: '已发布' }).first()
const title = (await row.locator('td').first().textContent()).trim()
await row.locator('button', { hasText: '归档' }).click()
await page.waitForTimeout(1500)
const updated = await page.evaluate(async (t) => {
const r = await fetch('/api/articles?page=1&size=50&status=ARCHIVED')
const j = await r.json()
const hit = j.data.list.find((a) => a.title === t)
return hit ? hit.status : null
}, title)
if (updated !== 'ARCHIVED') throw new Error('归档后状态=' + updated)
})
await step(browser, '2.5.8', '删除文章', async (page) => {
await login(page)
await page.waitForSelector('.article-table')
const unique = '待删除-' + Date.now()
await page.evaluate(async (t) => {
const token = localStorage.getItem('token')
const r = await fetch('/api/admin/articles', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token },
body: JSON.stringify({ title: t, content: 'to delete' })
})
const j = await r.json()
window.__delId = j.data.id
window.__delSlug = j.data.slug
}, unique)
await page.reload()
await page.waitForSelector('.filter-select')
await page.selectOption('.filter-select', { label: '草稿' })
await page.waitForSelector('.article-table')
await page.waitForTimeout(500)
await page.once('dialog', (d) => d.accept())
const row = page.locator('.article-table tbody tr', { hasText: unique }).first()
await row.locator('button', { hasText: '删除' }).click()
await page.waitForTimeout(1500)
const gone = await page.evaluate(async (slug) => {
const r = await fetch('/api/articles/' + slug)
const j = await r.json()
return j.code
}, await page.evaluate(() => window.__delSlug))
if (gone !== 3001 && gone !== 404) throw new Error('删除后公开可访问 code=' + gone)
})
await step(browser, '2.5.9', '标签管理: 新增/编辑/删除', async (page) => {
await login(page)
await page.goto(BASE + '/admin/tags')
await page.waitForSelector('.tag-table')
const unique = '验收标签-' + Date.now()
await page.click('text=新增标签')
await page.waitForSelector('.tag-form input')
await page.fill('.tag-form input:first-of-type', unique)
await page.click('.tag-form button[type=submit]')
await page.waitForTimeout(1000)
let body = await page.locator('.tag-table').textContent()
if (!body.includes(unique)) throw new Error('新增标签未显示')
await page.locator('.tag-table tbody tr', { hasText: unique }).locator('button', { hasText: '编辑' }).click()
await page.fill('.tag-form input:first-of-type', unique + '-改')
await page.click('.tag-form button[type=submit]')
await page.waitForTimeout(1000)
body = await page.locator('.tag-table').textContent()
if (!body.includes(unique + '-改')) throw new Error('编辑未生效')
await page.once('dialog', (d) => d.accept())
await page.locator('.tag-table tbody tr', { hasText: unique + '-改' }).locator('button', { hasText: '删除' }).click()
await page.waitForTimeout(1000)
body = await page.locator('.tag-table').textContent()
if (body.includes(unique)) throw new Error('删除未生效')
})
await step(browser, '2.5.10', '表单校验: 空标题/内容拦截', async (page) => {
await login(page)
await page.goto(BASE + '/admin/articles/new')
await page.waitForSelector('#title')
await page.fill('#title', ' ')
await page.fill('#content', ' ')
await page.click('button[type=submit]')
await page.waitForTimeout(500)
const err = await page.locator('.error-text').textContent()
if (!err || !err.includes('必填')) throw new Error('未提示必填: ' + err)
const url = page.url()
if (url.includes('admin/articles/new') === false) throw new Error('不应离开编辑器')
})
await step(browser, '2.6.1', '封面上传', async (page) => {
await login(page)
await page.goto(BASE + '/admin/articles/new')
await page.waitForSelector('#cover')
const buf = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', 'base64')
await page.setInputFiles('.upload-input', { name: 'cover.png', mimeType: 'image/png', buffer: buf })
await page.waitForFunction(() => document.querySelector('#cover')?.value.includes('/'), null, { timeout: 10000 })
const url = await page.locator('#cover').inputValue()
if (!url) throw new Error('上传后 URL 未填入')
const resp = await page.evaluate(async (u) => {
const r = await fetch(u)
return { status: r.status, ct: r.headers.get('content-type') }
}, url)
if (resp.status !== 200) throw new Error('上传文件不可访问 status=' + resp.status)
})
await step(browser, '2.6.2', '上传反馈(上传中状态)', async (page) => {
await login(page)
await page.goto(BASE + '/admin/articles/new')
await page.waitForSelector('#cover')
const buf = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', 'base64')
await page.setInputFiles('.upload-input', { name: 'cover2.png', mimeType: 'image/png', buffer: buf })
await page.waitForFunction(() => document.querySelector('.upload-btn')?.textContent.includes('上传中'), null, { timeout: 2000 }).catch(() => {})
await page.waitForFunction(() => !document.querySelector('.upload-btn')?.textContent.includes('上传中'), null, { timeout: 10000 })
const finalText = await page.locator('.upload-btn').textContent()
if (!finalText.includes('上传')) throw new Error('上传完成后未恢复按钮文字')
})
await browser.close()
console.log(JSON.stringify(results, null, 2))
console.log(`PASS=${passed} FAIL=${failed}`)
})().catch((e) => {
console.error('FATAL', e)
process.exit(1)
})