mp-crm-primitives.test.mjs 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import assert from 'node:assert/strict'
  2. import test from 'node:test'
  3. import { readFile } from 'node:fs/promises'
  4. const root = new URL('../', import.meta.url)
  5. const read = (path) => readFile(new URL(path, root), 'utf8')
  6. test('普通业务头仅提供固定标题与可选搜索,不暴露票根或动态摘要插槽', async () => {
  7. const source = await read('src/components/mp/MpBusinessHeader.vue')
  8. assert.match(source, /class="mp-wv-header"/)
  9. assert.match(source, /class="mp-wv-header__title"[^>]*>\{\{ title \}\}/)
  10. assert.match(source, /v-if="searchable"[\s\S]*?class="mp-wv-search\b/)
  11. assert.match(source, /name="before-search"/)
  12. assert.match(source, /name="after-search"/)
  13. assert.match(source, /\$slots\.actions[\s\S]*?name="actions"/)
  14. assert.doesNotMatch(source, /ticket|hero|mp-wv-header__sub/i)
  15. })
  16. test('状态 Tab 与三栏筛选保持独立局部 modifier,并给更多筛选红点', async () => {
  17. const [tabs, filters, legacyStyles, styles] = await Promise.all([
  18. read('src/components/mp/MpStatusTabs.vue'),
  19. read('src/components/mp/MpThreeColumnFilters.vue'),
  20. read('src/styles/mp-webview.scss'),
  21. read('src/styles/mp-crm-primitives.scss')
  22. ])
  23. assert.match(tabs, /class="mp-wv-status-tabs"/)
  24. assert.match(tabs, /class="filter-tag"/)
  25. assert.match(tabs, /<nav[^>]*class="mp-wv-status-tabs"/)
  26. assert.match(tabs, /:aria-pressed="tab\.value === modelValue"/)
  27. assert.match(filters, /mp-wv-filter-bar--three/)
  28. assert.match(filters, /mp-wv-filter-more/)
  29. assert.match(filters, /v-if="moreActive"[^>]*class="mp-wv-filter-more__dot"/)
  30. assert.match(legacyStyles, /\.mp-wv-filter-bar\s*\{[\s\S]*?grid-template-columns:\s*1fr 1fr;/)
  31. assert.match(
  32. styles,
  33. /\.mp-wv-filter-bar\.mp-wv-filter-bar--three\s*\{[\s\S]*?grid-template-columns:\s*repeat\(3, minmax\(0, 1fr\)\);/
  34. )
  35. assert.match(
  36. styles,
  37. /\.mp-wv-filter-wrap--business \.mp-wv-filter-opt\s*\{[\s\S]*?min-height:\s*44px;/
  38. )
  39. assert.match(
  40. styles,
  41. /\.mp-wv-filter-wrap--business > \.mp-wv-filter-mask\s*\{[\s\S]*?z-index:\s*7;/,
  42. '局部遮罩必须低于下拉面板,否则选项无法点击'
  43. )
  44. })
  45. test('备用金式卡片完整声明六层 DOM,状态标签使用浅底语义色', async () => {
  46. const [card, styles] = await Promise.all([
  47. read('src/components/mp/MpExpenseCard.vue'),
  48. read('src/styles/mp-crm-primitives.scss')
  49. ])
  50. for (const className of [
  51. 'expense-card',
  52. 'ec-top',
  53. 'status-tag',
  54. 'ec-amount',
  55. 'ec-emphasis',
  56. 'list-card-kv',
  57. 'ec-time',
  58. 'ec-bottom',
  59. 'op-actions'
  60. ]) {
  61. assert.match(card, new RegExp(`\\b${className}\\b`), `卡片缺少 ${className} 层`)
  62. }
  63. assert.doesNotMatch(card, /<article[\s\S]*?role="link"/, '卡片容器不得形成嵌套交互语义')
  64. assert.match(card, /class="expense-card__open mp-wv-touch-target"/)
  65. assert.match(styles, /\.status-tag--success[^{]*\{[\s\S]*?background:\s*#e9f9f2;/)
  66. const statusBlocks = [...styles.matchAll(/\.status-tag(?:--\w+)?[^{]*\{([^}]*)\}/g)]
  67. assert.ok(statusBlocks.length >= 5, '状态标签应声明基础样式与语义色')
  68. for (const [, declarations] of statusBlocks) {
  69. assert.doesNotMatch(declarations, /color:\s*#fff;/, '状态标签不得使用实心白字样式')
  70. }
  71. })
  72. test('附件、审批时间轴与通用触达尺寸满足移动端契约', async () => {
  73. const [attachments, timeline, styles] = await Promise.all([
  74. read('src/components/mp/MpAttachmentGrid.vue'),
  75. read('src/components/mp/MpApprovalTimeline.vue'),
  76. read('src/styles/mp-crm-primitives.scss')
  77. ])
  78. assert.match(attachments, /<ul class="mp-crm-att-grid" aria-label="附件列表"/)
  79. assert.match(attachments, /class="att-img"/)
  80. assert.match(attachments, /class="att-doc"/)
  81. assert.match(attachments, /<button class="att-add" type="button" @click="emit\('add'\)"/)
  82. assert.match(attachments, /:disabled="Boolean\(item\.uploading\) \|\| !item\.url"/)
  83. assert.match(
  84. attachments,
  85. /v-if="editable && !item\.uploading"[\s\S]*?class="att-icon att-icon--del"/
  86. )
  87. assert.match(
  88. attachments,
  89. /v-if="item\.url && !item\.uploading"[\s\S]*?class="att-icon att-icon--dl"/
  90. )
  91. assert.match(timeline, /class="mp-crm-approval-timeline"/)
  92. assert.match(timeline, /class="mp-crm-approval-timeline__avatar"/)
  93. assert.match(
  94. styles,
  95. /\.mp-crm-att-grid\s*\{[\s\S]*?grid-template-columns:\s*repeat\(auto-fill, minmax\(96px, 1fr\)\);/
  96. )
  97. assert.match(styles, /\.mp-crm-att-grid \.att-item\s*\{[\s\S]*?aspect-ratio:\s*1;/)
  98. assert.match(
  99. styles,
  100. /\.mp-crm-att-grid \.att-icon\s*\{[\s\S]*?width:\s*44px;[\s\S]*?height:\s*44px;/
  101. )
  102. assert.match(
  103. styles,
  104. /\.mp-wv-touch-target\s*\{[\s\S]*?min-width:\s*44px;[\s\S]*?min-height:\s*44px;/
  105. )
  106. })