import type { Page, Route } from '@playwright/test' import { captureScreenshot, expect, test } from './fixtures' /** * 内容制作列表工具栏回归。 * * 工具栏用显式筛选:关键词与三个下拉都要点「搜索」或在输入框回车才生效,「重置」清空全部条件; * 不放「刷新」按钮,也不重复显示总条数(条数由底部分页承担)。 * 关键词框比全局 390px 收窄三分之一到 260px,给下拉与按钮留横向空间。 * 「新建」动作落在工具栏右端,页面不使用 SystemPageHeader,白底内容区有最小高度。 */ const json = (route: Route, data: unknown) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, message: '成功', data, timestamp: Date.now(), requestId: 'e2e-toolbar' }), }) /** 记录每次列表查询的 keyword 与 status,用于断言即时筛选与防抖。 */ type ListQuery = { keyword: string; status: string } async function mockContentListApi(page: Page, queries: ListQuery[]) { await page.route(/\/api\/(?:auth|tran)\//, async (route) => { const url = new URL(route.request().url()) const path = url.pathname if (path.endsWith('/auth/me')) { return json(route, { userId: '1', username: 'admin', displayName: '系统管理员', departmentId: '110', departmentName: '信息中心', activeRoleId: '1', authorizationMode: 'SINGLE_ACTIVE', mustChangePassword: false, roles: [{ id: '1', code: 'ADMIN', name: '管理员', shortName: '管', enabled: true, builtIn: true, isSuperAdmin: true, dataScopeCode: 'ALL', version: 1 }], // 组件按 content.create / content.update / content.publish 判断动作权限, // 页面权限用 content.model;两套都要给,否则「新建」会退化成只读标签 permissions: ['content.model', 'content.create', 'content.update', 'content.delete', 'content.publish'], }) } if (path.endsWith('/content/projects/summary')) { return json(route, { total: 3, draft: 1, review: 0, published: 2, byType: [], byTrainingMode: [] }) } if (path.endsWith('/content/projects')) { queries.push({ keyword: url.searchParams.get('keyword') ?? '', status: url.searchParams.get('status') ?? '', }) return json(route, { records: [], total: 0, page: 1, size: 10 }) } if (path.endsWith('/auth/ping')) return json(route, { service: 'ut-auth', port: 6101, status: 'UP' }) if (path.endsWith('/tran/ping')) return json(route, { service: 'ut-tran', port: 6102, status: 'UP' }) return json(route, {}) }) } test('内容列表工具栏显式搜索重置、无刷新按钮且筛选项左对齐', async ({ page }, testInfo) => { const queries: ListQuery[] = [] await page.setViewportSize({ width: 1920, height: 900 }) await mockContentListApi(page, queries) await page.addInitScript(() => { window.sessionStorage.setItem('unreal-tran:web:access-token:v1', 'e2e-mock-token') }) await page.goto('/content/models') // 页头卡已去掉,没有 h1 可等;以列表卡片作为就绪锚点 await expect(page.locator('.content-list-card')).toBeVisible() const toolbar = page.locator('.content-filter') // 工具栏用显式的搜索与重置,不放刷新按钮 await expect(toolbar.getByRole('button', { name: '刷新' })).toHaveCount(0) const searchButton = toolbar.getByRole('button', { name: '搜索' }) const resetButton = toolbar.getByRole('button', { name: '重置' }) await expect(searchButton).toBeVisible() await expect(resetButton).toBeVisible() // 页头卡已去掉,「新建」动作落在工具条右端;总条数由底部分页承担,工具条里不再重复 await expect(page.locator('.system-page-header')).toHaveCount(0) await expect(toolbar.locator('.list-result-count')).toHaveCount(0) const createButton = toolbar.getByRole('button', { name: '新建模型' }) await expect(createButton).toBeVisible() const keywordInput = toolbar.locator('.el-input').first() const [toolbarBox, keywordBox, searchBox, resetBox, createBox] = await Promise.all([ toolbar.boundingBox(), keywordInput.boundingBox(), searchButton.boundingBox(), resetButton.boundingBox(), createButton.boundingBox(), ]) for (const box of [toolbarBox, keywordBox, searchBox, resetBox, createBox]) expect(box).not.toBeNull() // 关键词框比全局 390px 收窄三分之一 expect(keywordBox!.width).toBeGreaterThan(230) expect(keywordBox!.width).toBeLessThanOrEqual(270) // 搜索、重置按内容宽度显示,且和筛选控件一起靠左,不被 flex-grow 撑开 expect(searchBox!.width).toBeLessThanOrEqual(110) expect(resetBox!.width).toBeLessThanOrEqual(110) expect(resetBox!.x + resetBox!.width).toBeLessThan(toolbarBox!.x + toolbarBox!.width - 200) // 新建贴住工具条右端 expect(createBox!.x + createBox!.width).toBeGreaterThan(toolbarBox!.x + toolbarBox!.width - 40) // 白底内容区有最小高度,数据少时下方不出现大片空白 const cardBox = await page.locator('.content-list-card').boundingBox() expect(cardBox!.height).toBeGreaterThan(600) // 空态提供「清除筛选」,与工具栏的重置同一行为 const empty = page.locator('.asset-row-empty') await expect(empty).toContainText('没有符合条件的模型') await expect(empty.getByRole('button', { name: '清除筛选' })).toBeVisible() await captureScreenshot(page, testInfo, 'content-filter-explicit') // 只输入关键词不查询,必须点搜索或回车才发请求 const keywordRequests = () => queries.filter((item) => item.keyword === '挖掘机').length await toolbar.locator('input').first().fill('挖掘机') await page.waitForTimeout(700) expect(keywordRequests(), '未点搜索前不应发出查询').toBe(0) await searchButton.click() await expect.poll(keywordRequests, { timeout: 5_000 }).toBeGreaterThanOrEqual(1) // 下拉切换同样要点搜索才生效 const statusRequests = () => queries.filter((item) => item.status === 'PUBLISHED').length await toolbar.locator('.el-select').first().click() await page.getByRole('option', { name: '已发布', exact: true }).click() await page.waitForTimeout(500) expect(statusRequests(), '未点搜索前状态筛选不应生效').toBe(0) await searchButton.click() await expect.poll(statusRequests, { timeout: 5_000 }).toBeGreaterThanOrEqual(1) // 重置清空关键词并重新查询 await resetButton.click() await expect(toolbar.locator('input').first()).toHaveValue('') await expect.poll(() => queries.at(-1)?.keyword ?? 'x', { timeout: 5_000 }).toBe('') await page.setViewportSize({ width: 760, height: 900 }) await page.reload() await expect(page.locator('.content-list-card')).toBeVisible() const responsiveToolbar = page.locator('.content-filter') const hasHorizontalOverflow = await responsiveToolbar.evaluate((element) => element.scrollWidth > element.clientWidth + 1) expect(hasHorizontalOverflow).toBe(false) const responsiveSearchWidth = await responsiveToolbar.getByRole('button', { name: '搜索' }) .evaluate((element) => element.getBoundingClientRect().width) expect(responsiveSearchWidth).toBeLessThanOrEqual(110) await captureScreenshot(page, testInfo, 'content-filter-responsive') })