import assert from 'node:assert/strict' import test from 'node:test' import { readFile } from 'node:fs/promises' const root = new URL('../', import.meta.url) const read = (path) => readFile(new URL(path, root), 'utf8') test('普通业务头仅提供固定标题与可选搜索,不暴露票根或动态摘要插槽', async () => { const source = await read('src/components/mp/MpBusinessHeader.vue') assert.match(source, /class="mp-wv-header"/) assert.match(source, /class="mp-wv-header__title"[^>]*>\{\{ title \}\}/) assert.match(source, /v-if="searchable"[\s\S]*?class="mp-wv-search\b/) assert.match(source, /name="before-search"/) assert.match(source, /name="after-search"/) assert.match(source, /\$slots\.actions[\s\S]*?name="actions"/) assert.doesNotMatch(source, /ticket|hero|mp-wv-header__sub/i) }) test('状态 Tab 与三栏筛选保持独立局部 modifier,并给更多筛选红点', async () => { const [tabs, filters, legacyStyles, styles] = await Promise.all([ read('src/components/mp/MpStatusTabs.vue'), read('src/components/mp/MpThreeColumnFilters.vue'), read('src/styles/mp-webview.scss'), read('src/styles/mp-crm-primitives.scss') ]) assert.match(tabs, /class="mp-wv-status-tabs"/) assert.match(tabs, /class="filter-tag"/) assert.match(tabs, /]*class="mp-wv-status-tabs"/) assert.match(tabs, /:aria-pressed="tab\.value === modelValue"/) assert.match(filters, /mp-wv-filter-bar--three/) assert.match(filters, /mp-wv-filter-more/) assert.match(filters, /v-if="moreActive"[^>]*class="mp-wv-filter-more__dot"/) assert.match(legacyStyles, /\.mp-wv-filter-bar\s*\{[\s\S]*?grid-template-columns:\s*1fr 1fr;/) assert.match( styles, /\.mp-wv-filter-bar\.mp-wv-filter-bar--three\s*\{[\s\S]*?grid-template-columns:\s*repeat\(3, minmax\(0, 1fr\)\);/ ) assert.match( styles, /\.mp-wv-filter-wrap--business \.mp-wv-filter-opt\s*\{[\s\S]*?min-height:\s*44px;/ ) assert.match( styles, /\.mp-wv-filter-wrap--business > \.mp-wv-filter-mask\s*\{[\s\S]*?z-index:\s*7;/, '局部遮罩必须低于下拉面板,否则选项无法点击' ) }) test('备用金式卡片完整声明六层 DOM,状态标签使用浅底语义色', async () => { const [card, styles] = await Promise.all([ read('src/components/mp/MpExpenseCard.vue'), read('src/styles/mp-crm-primitives.scss') ]) for (const className of [ 'expense-card', 'ec-top', 'status-tag', 'ec-amount', 'ec-emphasis', 'list-card-kv', 'ec-time', 'ec-bottom', 'op-actions' ]) { assert.match(card, new RegExp(`\\b${className}\\b`), `卡片缺少 ${className} 层`) } assert.doesNotMatch(card, /= 5, '状态标签应声明基础样式与语义色') for (const [, declarations] of statusBlocks) { assert.doesNotMatch(declarations, /color:\s*#fff;/, '状态标签不得使用实心白字样式') } }) test('附件、审批时间轴与通用触达尺寸满足移动端契约', async () => { const [attachments, timeline, styles] = await Promise.all([ read('src/components/mp/MpAttachmentGrid.vue'), read('src/components/mp/MpApprovalTimeline.vue'), read('src/styles/mp-crm-primitives.scss') ]) assert.match(attachments, /class="att-grid\b/) assert.match(attachments, /class="att-img"/) assert.match(attachments, /class="att-doc-thumb"/) assert.match(attachments, /class="att-add mp-wv-touch-target"/) assert.match(timeline, /class="mp-crm-approval-timeline"/) assert.match(timeline, /class="mp-crm-approval-timeline__avatar"/) assert.match( styles, /\.mp-crm-att-grid \.att-(?:item|img|doc-thumb|add)[^{]*\{[\s\S]*?width:\s*72px;[\s\S]*?height:\s*72px;/ ) assert.match( styles, /\.mp-wv-touch-target\s*\{[\s\S]*?min-width:\s*44px;[\s\S]*?min-height:\s*44px;/ ) })