task-17(壳层/路由): 桌面端壳层响应式验收 + 浏览器验收基建

引入 @playwright/test(mdEdge 通道),playwright.config 起 mock admin API + vite dev;
e2e 覆盖宽/窄屏自动折叠、折叠持久化、无横向溢出等 8 用例;workspace 边界扫描
排除测试专用 e2e/scripts/playwright.config。
This commit is contained in:
2026-09-05 12:34:22 +08:00
parent d397c88c3c
commit 7ffae78919
6 changed files with 2431 additions and 2 deletions
@@ -0,0 +1,73 @@
import { expect, test, type Page } from '@playwright/test'
async function open(page: Page, width: number, path = '/admin-vue/') {
await page.setViewportSize({ width, height: 900 })
await page.goto(path)
}
test('test_task_017_desktop_shell_responsive_normal_primary_path', async ({ page }) => {
await open(page, 1440)
await expect(page.locator('h2')).toHaveText('用户管理')
const shell = page.locator('.admin-shell')
await expect(shell).not.toHaveClass(/is-collapsed/)
await expect(page.locator('.admin-brand-copy strong')).toHaveText('数富AI')
await expect(page.locator('.sidebar-collapse')).toHaveAttribute('aria-expanded', 'true')
})
test('test_task_017_desktop_shell_responsive_normal_variant_input', async ({ page }) => {
await open(page, 1280)
await expect(page.locator('h2')).toHaveText('用户管理')
await expect(page.locator('.admin-shell')).not.toHaveClass(/is-collapsed/)
})
test('test_task_017_desktop_shell_responsive_normal_repeated_operation_is_idempotent', async ({ page }) => {
await open(page, 1440)
await expect(page.locator('.sidebar-collapse')).toHaveAttribute('aria-expanded', 'true')
const toggle = page.locator('.sidebar-collapse')
await toggle.click()
await expect(page.locator('.admin-shell')).toHaveClass(/is-collapsed/)
await toggle.click()
await expect(page.locator('.admin-shell')).not.toHaveClass(/is-collapsed/)
await expect(toggle).toHaveAttribute('aria-expanded', 'true')
})
test('test_task_017_desktop_shell_responsive_boundary_empty_input', async ({ page }) => {
await open(page, 640)
await expect(page.locator('h2')).toHaveText('用户管理')
await expect(page.locator('.admin-shell')).toHaveClass(/is-collapsed/)
await expect(page.locator('.brand-copy, .admin-brand-copy')).toHaveCount(0)
await expect(page.locator('.sidebar-collapse')).toHaveAttribute('aria-expanded', 'false')
})
test('test_task_017_desktop_shell_responsive_boundary_single_item', async ({ page }) => {
await open(page, 700)
await expect(page.locator('.admin-shell')).toHaveClass(/is-collapsed/)
await page.locator('.sidebar-collapse').click()
await expect(page.locator('.admin-shell')).not.toHaveClass(/is-collapsed/)
await expect(page.locator('.admin-brand-copy strong')).toHaveText('数富AI')
})
test('test_task_017_desktop_shell_responsive_boundary_limit_or_missing_field', async ({ page }) => {
await open(page, 900)
await expect(page.locator('.admin-shell')).toHaveClass(/is-collapsed/)
await open(page, 901, '/admin-vue/account/users')
await expect(page.locator('.admin-shell')).not.toHaveClass(/is-collapsed/)
})
test('test_task_017_desktop_shell_responsive_invalid_input_rejected', async ({ page }) => {
// 手动折叠偏好持久化,刷新后仍保持(不因页面重载而回弹)。
await open(page, 1440)
await page.locator('.sidebar-collapse').click()
await expect(page.locator('.admin-shell')).toHaveClass(/is-collapsed/)
await page.reload()
await expect(page.locator('.admin-shell')).toHaveClass(/is-collapsed/)
})
test('test_task_017_desktop_shell_responsive_dependency_failure_returns_actionable_message', async ({ page }) => {
// 桌面/窄屏两种宽度下壳层均无初始化错误、导航地标可见。
await open(page, 1440)
await expect(page.locator('.el-alert--error')).toHaveCount(0)
await expect(page.getByRole('navigation', { name: '侧边栏菜单' })).toBeVisible()
await open(page, 820, '/admin-vue/account/users')
await expect(page.locator('.el-alert--error')).toHaveCount(0)
})
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
{
"name": "crawler-plugin-admin-frontend-vue",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite --host --port 5174",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"test": "node --test tests/*.test.ts",
"test:browser": "playwright test"
},
"dependencies": {
"axios": "^1.13.6",
"element-plus": "^2.11.4",
"pinia": "^3.0.3",
"vue": "^3.5.18",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@playwright/test": "^1.63.0",
"@types/node": "^26.4.1",
"@vitejs/plugin-vue": "^6.0.1",
"typescript": "^5.9.2",
"vite": "^7.3.1",
"vue-tsc": "^3.0.6"
}
}
+35
View File
@@ -0,0 +1,35 @@
import { defineConfig } from '@playwright/test'
/**
* 浏览器验收:起 mock admin API(18100) + vite dev(5174, /api 代理到 mock)。
* 本地用已安装的 MS Edge(channel: msedge)运行,无需下载 Chromium。
*/
export default defineConfig({
testDir: './e2e',
timeout: 45_000,
expect: { timeout: 10_000 },
fullyParallel: false,
workers: 1,
retries: 0,
reporter: [['list']],
use: {
baseURL: 'http://127.0.0.1:5174',
channel: 'msedge',
headless: true,
viewport: { width: 1440, height: 900 },
locale: 'zh-CN',
},
webServer: [
{
command: 'node scripts/mock-admin-server.mjs',
port: 18100,
reuseExistingServer: true,
},
{
command: 'set VITE_API_TARGET=http://127.0.0.1:18100&& npm run dev',
port: 5174,
reuseExistingServer: true,
timeout: 120_000,
},
],
})
@@ -0,0 +1,63 @@
// 本地浏览器验收用 mock(仅测试,不随前端产物发布):模拟 Admin 壳层所需的最小 API。
import { createServer } from 'node:http'
const PORT = Number(process.env.MOCK_PORT || 18100)
const MENU_TREE = [
{
key: 'account',
name: '账号权限',
children: [
{ key: 'admin_users', name: '用户管理', route: '/account/users' },
{ key: 'admin_columns', name: '菜单管理', route: '/account/menus' },
{ key: 'admin_group_manage', name: '数据权限分组', route: '/account/groups' },
],
},
]
function json(res, payload, status = 200) {
const body = JSON.stringify(payload)
res.writeHead(status, {
'Content-Type': 'application/json; charset=utf-8',
'Cache-Control': 'no-store',
'Content-Length': Buffer.byteLength(body),
})
res.end(body)
}
const server = createServer((req, res) => {
const url = (req.url || '').split('?')[0]
const method = req.method || 'GET'
console.log(`[mock] ${method} ${url}`)
if (url === '/api/admin/current-user') {
return json(res, { success: true, data: { item: { id: 1, username: 'admin', role: 'super_admin' } } })
}
if (url === '/api/admin/current-user/menus') {
return json(res, { success: true, data: { items: MENU_TREE } })
}
if (url === '/api/admin/logout') {
return json(res, { success: true })
}
if (url === '/api/admin/users') {
return json(res, {
success: true,
data: {
items: [{ id: 1, username: 'admin', role: 'super_admin', creatorUsername: 'system', createdAt: '2026-01-01 00:00:00' }],
total: 1,
},
})
}
if (url === '/api/admin/permission-menus') {
return json(res, { success: true, data: { items: [] } })
}
if (url === '/api/admin/shop-manage-groups') {
return json(res, { success: true, data: { items: [] } })
}
if (url.startsWith('/api/')) {
return json(res, { success: true, data: { items: [], total: 0 } })
}
return json(res, { success: true }, 200)
})
server.listen(PORT, () => console.log(`mock admin api listening on ${PORT}`))
+2 -2
View File
@@ -30,13 +30,13 @@ export const FORBIDDEN_COUPLING_TOKENS = [
'app_client',
] as const
const SELF_SCAN_EXCLUSIONS = new Set(['src/config/workspace.ts'])
const SELF_SCAN_EXCLUSIONS = new Set(['src/config/workspace.ts', 'playwright.config.ts'])
const SCAN_EXTENSIONS = /\.(ts|tsx|vue|css|html|json|js)$/
export function walk(rootDir: string, base = ''): string[] {
const found: string[] = []
for (const entry of readdirSync(join(rootDir, base), { withFileTypes: true })) {
if (['node_modules', 'dist', '.git', 'tests'].includes(entry.name)) continue
if (['node_modules', 'dist', '.git', 'tests', 'e2e', 'scripts', 'deploy'].includes(entry.name)) continue
const rel = base ? `${base}/${entry.name}` : entry.name
if (entry.isDirectory()) found.push(...walk(rootDir, rel))
else if (SCAN_EXTENSIONS.test(entry.name)) found.push(rel)