Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 

149 строки
7.2 KiB

  1. import type { Page, Route } from '@playwright/test'
  2. import { captureScreenshot, expect, test } from './fixtures'
  3. /**
  4. * 内容制作列表工具栏回归。
  5. *
  6. * 工具栏用显式筛选:关键词与三个下拉都要点「搜索」或在输入框回车才生效,「重置」清空全部条件;
  7. * 不放「刷新」按钮,也不重复显示总条数(条数由底部分页承担)。
  8. * 关键词框比全局 390px 收窄三分之一到 260px,给下拉与按钮留横向空间。
  9. * 「新建」动作落在工具栏右端,页面不使用 SystemPageHeader,白底内容区有最小高度。
  10. */
  11. const json = (route: Route, data: unknown) => route.fulfill({
  12. status: 200,
  13. contentType: 'application/json',
  14. body: JSON.stringify({ code: 0, message: '成功', data, timestamp: Date.now(), requestId: 'e2e-toolbar' }),
  15. })
  16. /** 记录每次列表查询的 keyword 与 status,用于断言即时筛选与防抖。 */
  17. type ListQuery = { keyword: string; status: string }
  18. async function mockContentListApi(page: Page, queries: ListQuery[]) {
  19. await page.route(/\/api\/(?:auth|tran)\//, async (route) => {
  20. const url = new URL(route.request().url())
  21. const path = url.pathname
  22. if (path.endsWith('/auth/me')) {
  23. return json(route, {
  24. userId: '1',
  25. username: 'admin',
  26. displayName: '系统管理员',
  27. departmentId: '110',
  28. departmentName: '信息中心',
  29. activeRoleId: '1',
  30. authorizationMode: 'SINGLE_ACTIVE',
  31. mustChangePassword: false,
  32. roles: [{ id: '1', code: 'ADMIN', name: '管理员', shortName: '管', enabled: true, builtIn: true, isSuperAdmin: true, dataScopeCode: 'ALL', version: 1 }],
  33. // 组件按 content.create / content.update / content.publish 判断动作权限,
  34. // 页面权限用 content.model;两套都要给,否则「新建」会退化成只读标签
  35. permissions: ['content.model', 'content.create', 'content.update', 'content.delete', 'content.publish'],
  36. })
  37. }
  38. if (path.endsWith('/content/projects/summary')) {
  39. return json(route, { total: 3, draft: 1, review: 0, published: 2, byType: [], byTrainingMode: [] })
  40. }
  41. if (path.endsWith('/content/projects')) {
  42. queries.push({
  43. keyword: url.searchParams.get('keyword') ?? '',
  44. status: url.searchParams.get('status') ?? '',
  45. })
  46. return json(route, { records: [], total: 0, page: 1, size: 10 })
  47. }
  48. if (path.endsWith('/auth/ping')) return json(route, { service: 'ut-auth', port: 6101, status: 'UP' })
  49. if (path.endsWith('/tran/ping')) return json(route, { service: 'ut-tran', port: 6102, status: 'UP' })
  50. return json(route, {})
  51. })
  52. }
  53. test('内容列表工具栏显式搜索重置、无刷新按钮且筛选项左对齐', async ({ page }, testInfo) => {
  54. const queries: ListQuery[] = []
  55. await page.setViewportSize({ width: 1920, height: 900 })
  56. await mockContentListApi(page, queries)
  57. await page.addInitScript(() => {
  58. window.sessionStorage.setItem('unreal-tran:web:access-token:v1', 'e2e-mock-token')
  59. })
  60. await page.goto('/content/models')
  61. // 页头卡已去掉,没有 h1 可等;以列表卡片作为就绪锚点
  62. await expect(page.locator('.content-list-card')).toBeVisible()
  63. const toolbar = page.locator('.content-filter')
  64. // 工具栏用显式的搜索与重置,不放刷新按钮
  65. await expect(toolbar.getByRole('button', { name: '刷新' })).toHaveCount(0)
  66. const searchButton = toolbar.getByRole('button', { name: '搜索' })
  67. const resetButton = toolbar.getByRole('button', { name: '重置' })
  68. await expect(searchButton).toBeVisible()
  69. await expect(resetButton).toBeVisible()
  70. // 页头卡已去掉,「新建」动作落在工具条右端;总条数由底部分页承担,工具条里不再重复
  71. await expect(page.locator('.system-page-header')).toHaveCount(0)
  72. await expect(toolbar.locator('.list-result-count')).toHaveCount(0)
  73. const createButton = toolbar.getByRole('button', { name: '新建模型' })
  74. await expect(createButton).toBeVisible()
  75. const keywordInput = toolbar.locator('.el-input').first()
  76. const [toolbarBox, keywordBox, searchBox, resetBox, createBox] = await Promise.all([
  77. toolbar.boundingBox(),
  78. keywordInput.boundingBox(),
  79. searchButton.boundingBox(),
  80. resetButton.boundingBox(),
  81. createButton.boundingBox(),
  82. ])
  83. for (const box of [toolbarBox, keywordBox, searchBox, resetBox, createBox]) expect(box).not.toBeNull()
  84. // 关键词框比全局 390px 收窄三分之一
  85. expect(keywordBox!.width).toBeGreaterThan(230)
  86. expect(keywordBox!.width).toBeLessThanOrEqual(270)
  87. // 搜索、重置按内容宽度显示,且和筛选控件一起靠左,不被 flex-grow 撑开
  88. expect(searchBox!.width).toBeLessThanOrEqual(110)
  89. expect(resetBox!.width).toBeLessThanOrEqual(110)
  90. expect(resetBox!.x + resetBox!.width).toBeLessThan(toolbarBox!.x + toolbarBox!.width - 200)
  91. // 新建贴住工具条右端
  92. expect(createBox!.x + createBox!.width).toBeGreaterThan(toolbarBox!.x + toolbarBox!.width - 40)
  93. // 白底内容区有最小高度,数据少时下方不出现大片空白
  94. const cardBox = await page.locator('.content-list-card').boundingBox()
  95. expect(cardBox!.height).toBeGreaterThan(600)
  96. // 空态提供「清除筛选」,与工具栏的重置同一行为
  97. const empty = page.locator('.asset-row-empty')
  98. await expect(empty).toContainText('没有符合条件的模型')
  99. await expect(empty.getByRole('button', { name: '清除筛选' })).toBeVisible()
  100. await captureScreenshot(page, testInfo, 'content-filter-explicit')
  101. // 只输入关键词不查询,必须点搜索或回车才发请求
  102. const keywordRequests = () => queries.filter((item) => item.keyword === '挖掘机').length
  103. await toolbar.locator('input').first().fill('挖掘机')
  104. await page.waitForTimeout(700)
  105. expect(keywordRequests(), '未点搜索前不应发出查询').toBe(0)
  106. await searchButton.click()
  107. await expect.poll(keywordRequests, { timeout: 5_000 }).toBeGreaterThanOrEqual(1)
  108. // 下拉切换同样要点搜索才生效
  109. const statusRequests = () => queries.filter((item) => item.status === 'PUBLISHED').length
  110. await toolbar.locator('.el-select').first().click()
  111. await page.getByRole('option', { name: '已发布', exact: true }).click()
  112. await page.waitForTimeout(500)
  113. expect(statusRequests(), '未点搜索前状态筛选不应生效').toBe(0)
  114. await searchButton.click()
  115. await expect.poll(statusRequests, { timeout: 5_000 }).toBeGreaterThanOrEqual(1)
  116. // 重置清空关键词并重新查询
  117. await resetButton.click()
  118. await expect(toolbar.locator('input').first()).toHaveValue('')
  119. await expect.poll(() => queries.at(-1)?.keyword ?? 'x', { timeout: 5_000 }).toBe('')
  120. await page.setViewportSize({ width: 760, height: 900 })
  121. await page.reload()
  122. await expect(page.locator('.content-list-card')).toBeVisible()
  123. const responsiveToolbar = page.locator('.content-filter')
  124. const hasHorizontalOverflow = await responsiveToolbar.evaluate((element) => element.scrollWidth > element.clientWidth + 1)
  125. expect(hasHorizontalOverflow).toBe(false)
  126. const responsiveSearchWidth = await responsiveToolbar.getByRole('button', { name: '搜索' })
  127. .evaluate((element) => element.getBoundingClientRect().width)
  128. expect(responsiveSearchWidth).toBeLessThanOrEqual(110)
  129. await captureScreenshot(page, testInfo, 'content-filter-responsive')
  130. })