import assert from 'node:assert/strict' import { mkdir } from 'node:fs/promises' import { chromium } from 'playwright' const origin = process.env.MP_CRM_PRIMITIVES_ORIGIN || 'http://localhost:3006' const artifacts = '/tmp/shanxia-mp-crm-primitives' await mkdir(artifacts, { recursive: true }) const browser = await chromium.launch({ headless: true, channel: 'chrome' }) const context = await browser.newContext({ viewport: { width: 375, height: 812 }, isMobile: true, deviceScaleFactor: 1 }) const page = await context.newPage() const pageErrors = [] page.on('pageerror', (error) => pageErrors.push(error.message)) try { await page.goto(origin, { waitUntil: 'domcontentloaded' }) await page.evaluate(async () => { const [ { createApp, h, ref }, { default: Header }, { default: Tabs }, { default: Filters }, { default: Card }, { default: Attachments }, { default: Timeline } ] = await Promise.all([ import('/node_modules/.vite/deps/vue.js'), import('/src/components/mp/MpBusinessHeader.vue'), import('/src/components/mp/MpStatusTabs.vue'), import('/src/components/mp/MpThreeColumnFilters.vue'), import('/src/components/mp/MpExpenseCard.vue'), import('/src/components/mp/MpAttachmentGrid.vue'), import('/src/components/mp/MpApprovalTimeline.vue'), import('/src/styles/mp-webview.scss') ]) document.body.innerHTML = '
' document.body.style.margin = '0' window.__primitiveEvents = [] createApp({ setup() { const keyword = ref('') const status = ref('all') const quickFilters = ref([ { key: 'owner', label: '负责人', value: 'all', options: [ { label: '全部负责人', value: 'all' }, { label: '测试用户', value: 'user-1' } ] }, { key: 'date', label: '创建时间', value: 'all', options: [ { label: '全部时间', value: 'all' }, { label: '本月', value: 'month' } ] } ]) const event = (name, payload) => window.__primitiveEvents.push({ name, payload }) return () => h( 'main', { class: 'primitive-page', style: { boxSizing: 'border-box', width: '100%', padding: '0 10px 24px', overflowX: 'clip' } }, [ h(Header, { title: '报价管理', searchable: true, keyword: keyword.value, searchPlaceholder: '搜索报价单号或客户', 'onUpdate:keyword': (value) => (keyword.value = value), onSearch: () => event('search', keyword.value), onBack: () => event('back') }), h(Tabs, { modelValue: status.value, tabs: [ { label: '全部', value: 'all', count: 2 }, { label: '草稿', value: 'draft', count: 1 } ], 'onUpdate:modelValue': (value) => (status.value = value), onChange: (value) => event('status', value) }), h(Filters, { quickFilters: quickFilters.value, moreActive: true, onSelect: (payload) => event('filter', payload), onMore: () => event('more') }), h( Card, { title: 'BJ20261017001', status: '审批中', statusTone: 'warning', amount: '¥ 12,680.00', amountLabel: '报价金额', time: '2026-10-17 10:30', onOpen: () => event('open') }, { emphasis: () => h('span', '山峡化工测试客户'), kv: () => [h('div', '有效期:30 天'), h('div', '负责人:测试用户')], actions: () => h('button', { type: 'button' }, '编辑') } ), h(Attachments, { items: [{ id: '1', url: '/fixture.pdf', name: '报价附件.pdf' }], editable: true, onAdd: () => event('attachment-add') }), h(Timeline, { items: [ { id: '1', name: '张审批', status: '已通过', tone: 'success', opinion: '同意', time: '2026-10-17 11:00' } ] }), h('section', { class: 'legacy-filter-smoke', 'aria-label': '历史两栏筛选冒烟' }, [ h('div', { class: 'mp-wv-filter-bar' }, [ h('button', { class: 'mp-wv-filter-trig', type: 'button' }, '历史筛选一'), h('button', { class: 'mp-wv-filter-trig', type: 'button' }, '历史筛选二') ]) ]) ] ) } }).mount('#primitive-fixture') }) await page.locator('.primitive-page').waitFor() const searchBox = page.locator('.mp-wv-search') assert.equal(await searchBox.getAttribute('aria-label'), '搜索') assert.ok((await searchBox.boundingBox()).height >= 44, '搜索框触达高度不足 44px') await searchBox.fill('测试报价') await searchBox.press('Enter') await page.locator('.mp-wv-status-tabs .filter-tag').nth(1).click() assert.equal( await page.locator('.mp-wv-status-tabs .filter-tag').nth(1).getAttribute('aria-pressed'), 'true' ) const firstFilter = page.locator('.mp-wv-filter-trig').first() await firstFilter.click() const option = page.locator('.mp-wv-filter-panel .mp-wv-filter-opt').nth(1) await option.waitFor() const optionIsTopmost = await option.evaluate((element) => { const box = element.getBoundingClientRect() const topmost = document.elementFromPoint(box.left + box.width / 2, box.top + box.height / 2) return topmost === element || element.contains(topmost) }) assert.equal(optionIsTopmost, true, '筛选遮罩覆盖了下拉选项') assert.ok((await option.boundingBox()).height >= 44, '筛选选项触达高度不足 44px') await option.click() assert.equal( await page.locator('.mp-wv-filter-panel').getAttribute('class'), 'mp-wv-filter-panel' ) await firstFilter.click() await page.locator('.mp-wv-filter-mask.open').waitFor() await page.mouse.click(5, 760) assert.equal(await firstFilter.getAttribute('aria-expanded'), 'false') const cardOpen = page.locator('.mp-crm-expense-card .expense-card__open') await cardOpen.focus() await cardOpen.press('Enter') await cardOpen.press('Space') const eventNames = await page.evaluate(() => window.__primitiveEvents.map((item) => item.name)) assert.deepEqual(eventNames.slice(0, 5), ['search', 'status', 'filter', 'open', 'open']) const legacyColumns = await page .locator('.legacy-filter-smoke .mp-wv-filter-bar') .evaluate((element) => getComputedStyle(element).gridTemplateColumns.split(' ').length) assert.equal(legacyColumns, 2, '历史筛选栏不应被改成三列') const attachmentBox = await page.locator('.mp-crm-att-grid .att-item').first().boundingBox() assert.equal(attachmentBox.width, 72) assert.equal(attachmentBox.height, 72) await page.evaluate(() => document.activeElement?.blur()) const viewportGeometry = await page.evaluate(() => ({ clientWidth: document.documentElement.clientWidth, scrollWidth: document.documentElement.scrollWidth })) assert.equal(viewportGeometry.clientWidth, 375) assert.ok( viewportGeometry.scrollWidth <= viewportGeometry.clientWidth, `公共原语出现横向溢出:${JSON.stringify(viewportGeometry)}` ) await page.screenshot({ path: `${artifacts}/primitives-375.png`, fullPage: true }) assert.deepEqual(pageErrors, [], `页面异常:${pageErrors.join(';')}`) console.log('CRM 公共原语浏览器交互回归通过:搜索、Tab、筛选、键盘开卡、附件与历史两栏冒烟') } finally { await browser.close() }