test-mp-crm-primitives.mjs 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. import assert from 'node:assert/strict'
  2. import { mkdir } from 'node:fs/promises'
  3. import { chromium } from 'playwright'
  4. const origin = process.env.MP_CRM_PRIMITIVES_ORIGIN || 'http://localhost:3006'
  5. const artifacts = '/tmp/shanxia-mp-crm-primitives'
  6. await mkdir(artifacts, { recursive: true })
  7. const browser = await chromium.launch({ headless: true, channel: 'chrome' })
  8. const context = await browser.newContext({
  9. viewport: { width: 375, height: 812 },
  10. isMobile: true,
  11. deviceScaleFactor: 1
  12. })
  13. const page = await context.newPage()
  14. const pageErrors = []
  15. page.on('pageerror', (error) => pageErrors.push(error.message))
  16. try {
  17. await page.goto(origin, { waitUntil: 'domcontentloaded' })
  18. await page.evaluate(async () => {
  19. const [
  20. { createApp, h, ref },
  21. { default: Header },
  22. { default: Tabs },
  23. { default: Filters },
  24. { default: Card },
  25. { default: Attachments },
  26. { default: Timeline }
  27. ] = await Promise.all([
  28. import('/node_modules/.vite/deps/vue.js'),
  29. import('/src/components/mp/MpBusinessHeader.vue'),
  30. import('/src/components/mp/MpStatusTabs.vue'),
  31. import('/src/components/mp/MpThreeColumnFilters.vue'),
  32. import('/src/components/mp/MpExpenseCard.vue'),
  33. import('/src/components/mp/MpAttachmentGrid.vue'),
  34. import('/src/components/mp/MpApprovalTimeline.vue'),
  35. import('/src/styles/mp-webview.scss')
  36. ])
  37. document.body.innerHTML = '<div id="primitive-fixture"></div>'
  38. document.body.style.margin = '0'
  39. window.__primitiveEvents = []
  40. createApp({
  41. setup() {
  42. const keyword = ref('')
  43. const status = ref('all')
  44. const quickFilters = ref([
  45. {
  46. key: 'owner',
  47. label: '负责人',
  48. value: 'all',
  49. options: [
  50. { label: '全部负责人', value: 'all' },
  51. { label: '测试用户', value: 'user-1' }
  52. ]
  53. },
  54. {
  55. key: 'date',
  56. label: '创建时间',
  57. value: 'all',
  58. options: [
  59. { label: '全部时间', value: 'all' },
  60. { label: '本月', value: 'month' }
  61. ]
  62. }
  63. ])
  64. const event = (name, payload) => window.__primitiveEvents.push({ name, payload })
  65. return () =>
  66. h(
  67. 'main',
  68. {
  69. class: 'primitive-page',
  70. style: {
  71. boxSizing: 'border-box',
  72. width: '100%',
  73. padding: '0 10px 24px',
  74. overflowX: 'clip'
  75. }
  76. },
  77. [
  78. h(Header, {
  79. title: '报价管理',
  80. searchable: true,
  81. keyword: keyword.value,
  82. searchPlaceholder: '搜索报价单号或客户',
  83. 'onUpdate:keyword': (value) => (keyword.value = value),
  84. onSearch: () => event('search', keyword.value),
  85. onBack: () => event('back')
  86. }),
  87. h(Tabs, {
  88. modelValue: status.value,
  89. tabs: [
  90. { label: '全部', value: 'all', count: 2 },
  91. { label: '草稿', value: 'draft', count: 1 }
  92. ],
  93. 'onUpdate:modelValue': (value) => (status.value = value),
  94. onChange: (value) => event('status', value)
  95. }),
  96. h(Filters, {
  97. quickFilters: quickFilters.value,
  98. moreActive: true,
  99. onSelect: (payload) => event('filter', payload),
  100. onMore: () => event('more')
  101. }),
  102. h(
  103. Card,
  104. {
  105. title: 'BJ20261017001',
  106. status: '审批中',
  107. statusTone: 'warning',
  108. amount: '¥ 12,680.00',
  109. amountLabel: '报价金额',
  110. time: '2026-10-17 10:30',
  111. onOpen: () => event('open')
  112. },
  113. {
  114. emphasis: () => h('span', '山峡化工测试客户'),
  115. kv: () => [h('div', '有效期:30 天'), h('div', '负责人:测试用户')],
  116. actions: () => h('button', { type: 'button' }, '编辑')
  117. }
  118. ),
  119. h(Attachments, {
  120. items: [{ id: '1', url: '/fixture.pdf', name: '报价附件.pdf' }],
  121. editable: true,
  122. onAdd: () => event('attachment-add')
  123. }),
  124. h(Timeline, {
  125. items: [
  126. {
  127. id: '1',
  128. name: '张审批',
  129. status: '已通过',
  130. tone: 'success',
  131. opinion: '同意',
  132. time: '2026-10-17 11:00'
  133. }
  134. ]
  135. }),
  136. h('section', { class: 'legacy-filter-smoke', 'aria-label': '历史两栏筛选冒烟' }, [
  137. h('div', { class: 'mp-wv-filter-bar' }, [
  138. h('button', { class: 'mp-wv-filter-trig', type: 'button' }, '历史筛选一'),
  139. h('button', { class: 'mp-wv-filter-trig', type: 'button' }, '历史筛选二')
  140. ])
  141. ])
  142. ]
  143. )
  144. }
  145. }).mount('#primitive-fixture')
  146. })
  147. await page.locator('.primitive-page').waitFor()
  148. const searchBox = page.locator('.mp-wv-search')
  149. assert.equal(await searchBox.getAttribute('aria-label'), '搜索')
  150. assert.ok((await searchBox.boundingBox()).height >= 44, '搜索框触达高度不足 44px')
  151. await searchBox.fill('测试报价')
  152. await searchBox.press('Enter')
  153. await page.locator('.mp-wv-status-tabs .filter-tag').nth(1).click()
  154. assert.equal(
  155. await page.locator('.mp-wv-status-tabs .filter-tag').nth(1).getAttribute('aria-pressed'),
  156. 'true'
  157. )
  158. const firstFilter = page.locator('.mp-wv-filter-trig').first()
  159. await firstFilter.click()
  160. const option = page.locator('.mp-wv-filter-panel .mp-wv-filter-opt').nth(1)
  161. await option.waitFor()
  162. const optionIsTopmost = await option.evaluate((element) => {
  163. const box = element.getBoundingClientRect()
  164. const topmost = document.elementFromPoint(box.left + box.width / 2, box.top + box.height / 2)
  165. return topmost === element || element.contains(topmost)
  166. })
  167. assert.equal(optionIsTopmost, true, '筛选遮罩覆盖了下拉选项')
  168. assert.ok((await option.boundingBox()).height >= 44, '筛选选项触达高度不足 44px')
  169. await option.click()
  170. assert.equal(
  171. await page.locator('.mp-wv-filter-panel').getAttribute('class'),
  172. 'mp-wv-filter-panel'
  173. )
  174. await firstFilter.click()
  175. await page.locator('.mp-wv-filter-mask.open').waitFor()
  176. await page.mouse.click(5, 760)
  177. assert.equal(await firstFilter.getAttribute('aria-expanded'), 'false')
  178. const cardOpen = page.locator('.mp-crm-expense-card .expense-card__open')
  179. await cardOpen.focus()
  180. await cardOpen.press('Enter')
  181. await cardOpen.press('Space')
  182. const eventNames = await page.evaluate(() => window.__primitiveEvents.map((item) => item.name))
  183. assert.deepEqual(eventNames.slice(0, 5), ['search', 'status', 'filter', 'open', 'open'])
  184. const legacyColumns = await page
  185. .locator('.legacy-filter-smoke .mp-wv-filter-bar')
  186. .evaluate((element) => getComputedStyle(element).gridTemplateColumns.split(' ').length)
  187. assert.equal(legacyColumns, 2, '历史筛选栏不应被改成三列')
  188. const attachmentBox = await page.locator('.mp-crm-att-grid .att-item').first().boundingBox()
  189. assert.equal(attachmentBox.width, 72)
  190. assert.equal(attachmentBox.height, 72)
  191. await page.evaluate(() => document.activeElement?.blur())
  192. const viewportGeometry = await page.evaluate(() => ({
  193. clientWidth: document.documentElement.clientWidth,
  194. scrollWidth: document.documentElement.scrollWidth
  195. }))
  196. assert.equal(viewportGeometry.clientWidth, 375)
  197. assert.ok(
  198. viewportGeometry.scrollWidth <= viewportGeometry.clientWidth,
  199. `公共原语出现横向溢出:${JSON.stringify(viewportGeometry)}`
  200. )
  201. await page.screenshot({ path: `${artifacts}/primitives-375.png`, fullPage: true })
  202. assert.deepEqual(pageErrors, [], `页面异常:${pageErrors.join(';')}`)
  203. console.log('CRM 公共原语浏览器交互回归通过:搜索、Tab、筛选、键盘开卡、附件与历史两栏冒烟')
  204. } finally {
  205. await browser.close()
  206. }