This commit is contained in:
2026-09-14 17:54:21 +08:00
parent 7e1b9b4cee
commit 25825685c3
40 changed files with 850 additions and 705 deletions
+147
View File
@@ -0,0 +1,147 @@
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { compileStyle, parse } from 'vue/compiler-sfc'
// Approved entry/consumer inventory. Do not discover unrelated menus recursively.
const consumers = [
'customers/pages/CustomersView.vue', 'customers/pages/CustomerDetailView.vue',
'customers/pages/OverseasStorageFeesView.vue', 'customers/components/CustomerBillingRulesPanel.vue',
'customers/CustomerOverseasStorageFeePanel.vue', 'unified-orders/pages/UnifiedOrdersView.vue',
'inbounds/components/InboundTaskList.vue', 'inbounds/measurement/InboundMeasurementDialog.vue',
'inbounds/measurement/InboundMeasurementPanel.vue', 'inbounds/review/MeasurementReviewPanel.vue',
'inbounds/weight/components/WeightOcrPanel.vue', 'loading-lists/pages/PreloadPlanDetailView.vue',
'loading-lists/components/LoadingListsView.vue', 'loading-lists/components/LoadingListDetailDrawer.vue',
'loading-lists/components/PalletLabelPrintDialog.vue', 'warehouse-resources/locations/pages/LocationsView.vue',
'warehouse-resources/pallets/pages/PalletsView.vue', 'domestic-containers/pages/DomesticContainersView.vue',
'domestic-containers/pages/DomesticContainerInventoryDetailView.vue',
'container-loading/components/ContainerSimulationCreate.vue', 'audit/components/CustomerAuditLogsPanel.vue',
]
const moduleCss = ['customers/customerDensity.css', 'unified-orders/styles/orderDensity.css', 'inbounds/measurement/measurementDensity.css', 'domestic-containers/pages/domesticContainersSummary.css']
const read = (file: string) => readFileSync(resolve(process.cwd(), 'src', file), 'utf8')
const domesticPages = [
{ title: '订单列表', path: '/orders', file: 'unified-orders/pages/UnifiedOrdersView.vue' },
{ title: '订单测量', path: '/inbounds/measurements', file: 'inbounds/components/InboundTaskList.vue', entry: 'inbounds/pages/InboundMeasurementsView.vue', component: 'InboundTaskList', mode: 'measurement' },
{ title: '测量数据审核', path: '/inbounds/measurement-reviews', file: 'inbounds/components/InboundTaskList.vue', entry: 'inbounds/pages/MeasurementReviewsView.vue', component: 'InboundTaskList', mode: 'review' },
{ title: '预装清单', path: '/loading-plans', file: 'loading-lists/components/LoadingListsView.vue', entry: 'loading-lists/pages/PreloadPlansView.vue', component: 'LoadingListsView', mode: 'preload' },
{ title: '库位管理', path: '/master-data/locations', file: 'warehouse-resources/locations/pages/LocationsView.vue' },
{ title: '托盘管理', path: '/master-data/pallets', file: 'warehouse-resources/pallets/pages/PalletsView.vue' },
{ title: '整柜业务', path: '/domestic-containers', file: 'domestic-containers/pages/DomesticContainersView.vue' },
]
const densityPages = [...new Set([
...domesticPages.map(page => page.file), 'loading-lists/pages/PreloadPlanDetailView.vue',
'loading-lists/components/LoadingListDetailDrawer.vue', 'domestic-containers/pages/DomesticContainerInventoryDetailView.vue',
'customers/pages/CustomersView.vue', 'customers/pages/CustomerDetailView.vue', 'customers/pages/OverseasStorageFeesView.vue',
])]
type TemplateNode = NonNullable<NonNullable<ReturnType<typeof parse>['descriptor']['template']>['ast']>['children'][number]
type ElementNode = Extract<TemplateNode, { type: 1 }>
const classesOf = (node: ElementNode, preload = true): string[] => node.props.flatMap(prop => {
if (prop.type === 6 && prop.name === 'class') return prop.value?.content.split(/\s+/) ?? []
if (prop.type !== 7 || prop.name !== 'bind' || prop.arg?.type !== 4 || prop.arg.content !== 'class' || prop.exp?.type !== 4 || !prop.exp.content.includes('density')) return []
// Evaluate only the authored density class binding with the shared component's two modes.
const value = new Function('mode', 'isPreload', 'dense', `return (${prop.exp.content})`)(preload ? 'preload' : 'confirmed', preload, preload) as Record<string, boolean>
return Object.entries(value).filter(([, enabled]) => enabled).flatMap(([names]) => names.split(/\s+/))
})
const styles = [
...consumers.flatMap(file => parse(read(`modules/${file}`)).descriptor.styles.map(style => ({ file, css: style.content }))),
...moduleCss.map(file => ({ file, css: read(`modules/${file}`) })),
]
const shared = read('styles/adminDensity.css')
const tokens = read('styles/adminDensityTokens.css')
const cssRoot = (css: string) => {
const result = compileStyle({ source: css, filename: 'density.css', id: 'density-contract', trim: false })
expect(result.errors).toEqual([])
return result.rawResult!.root
}
describe('approved Admin density ownership', () => {
it('keeps shared list actions and detail content inside a density-enabled page ancestor', () => {
for (const file of densityPages) {
const template = parse(read(`modules/${file}`)).descriptor.template!.ast!
let checked = 0
const visit = (nodes: typeof template.children, insideDensity = false) => {
for (const node of nodes) {
if (node.type !== 1) continue
const classes = classesOf(node)
if (node.tag === 'DataTable' || node.tag === 'LoadingListDetailDrawer' || classes.some(name => ['density-action', 'density-actions', 'density-tabs', 'density-fields', 'density-tab-icon'].includes(name))) {
expect(insideDensity, `${file}: ${classes.join(' ')} lacks its density ancestor`).toBe(true)
checked += 1
}
visit(node.children, insideDensity || classes.some(name => ['admin-density', 'admin-density-dialog'].includes(name)))
}
}
visit(template.children)
expect(checked, `${file}: no shared styles checked`).toBeGreaterThan(0)
}
})
it('covers all seven domestic menu entries and loads density CSS on direct entry', () => {
const navigation = read('core/navigation/adminNavigation.ts')
const domestic = navigation.slice(navigation.indexOf("key: 'domestic-warehouse'"), navigation.indexOf("key: 'overseas-delivery'"))
expect([...domestic.matchAll(/path: '/g)]).toHaveLength(domesticPages.length)
for (const page of domesticPages) {
expect(domestic).toContain(`title: '${page.title}'`)
expect(domestic).toContain(`path: '${page.path}'`)
if (page.entry) expect(read(`modules/${page.entry}`)).toContain(`<${page.component} mode="${page.mode}"`)
}
for (const file of densityPages) expect(read(`modules/${file}`), `${file}: direct entry CSS`).toContain('@/styles/adminDensity.css')
})
it('uses shared outlined actions across domestic tables and explicit classes on teleported dialogs', () => {
for (const file of densityPages) {
const template = parse(read(`modules/${file}`)).descriptor.template!.ast!
const visit = (nodes: typeof template.children, actionSlot = false, parentTag = '') => {
for (const node of nodes) {
if (node.type !== 1) continue
const isActionSlot = actionSlot || (parentTag === 'DataTable' && node.props.some(prop => prop.type === 7 && prop.name === 'slot' && prop.arg?.type === 4 && prop.arg.content === 'actions'))
if (isActionSlot && node.tag === 'el-button') expect(classesOf(node), `${file}: row action`).toContain('density-action')
if (['AppDialog', 'el-dialog'].includes(node.tag)) expect(classesOf(node), `${file}: teleported dialog`).toContain('admin-density-dialog')
visit(node.children, isActionSlot, node.tag)
}
}
visit(template.children)
}
})
it('keeps tokens and shared rules opt-in and free of business selectors', () => {
expect(shared).toContain("@import './adminDensityTokens.css'")
for (const css of [tokens, shared]) cssRoot(css).walkRules(rule => {
expect(rule.selector, `unscoped rule: ${rule.selector}`).toContain('.admin-density')
expect(rule.selector).not.toMatch(/customer-|order-|inbound-|preload-/)
})
})
it('defines every referenced density variable once, with local field width as the documented exception', () => {
const definitions = [...tokens.matchAll(/(--admin-[\w-]+)\s*:/g)].map(match => match[1])
expect(new Set(definitions).size).toBe(definitions.length)
const declared = new Set([...definitions, '--admin-field-label-width'])
for (const { file, css } of [...styles, { file: 'adminDensity.css', css: shared }]) {
for (const match of css.matchAll(/var\((--admin-[\w-]+)/g)) expect(declared.has(match[1]!), `${file}: ${match[1]}`).toBe(true)
for (const match of css.matchAll(/(--admin-[\w-]+)\s*:/g)) expect(match[1], `${file}: token redefinition`).toBe('--admin-field-label-width')
}
})
it('prevents page copies of action paint, tab primitives and desktop density dimensions', () => {
for (const { file, css } of styles) cssRoot(css).walkRules(rule => {
expect(rule.selector, file).not.toMatch(/customer-action-button|order-action-button|\.density-action|\.density-tab-icon/)
let desktop = true
for (let parent: typeof rule.parent | ReturnType<typeof cssRoot>['parent'] = rule.parent; parent; parent = parent.parent) {
if (parent.type === 'atrule' && /max-width|print/.test('params' in parent ? String(parent.params) : '')) desktop = false
}
rule.walkDecls(decl => {
expect(decl.prop, file).not.toMatch(/^--el-button-/)
if (desktop && decl.prop === 'font-size') expect(decl.value, `${file}: ${rule.selector}`).not.toMatch(/^(10|11|12|13|14|16)px$/)
if (desktop && /^(height|min-height)$/.test(decl.prop)) expect(decl.value, `${file}: ${rule.selector}`).not.toMatch(/^(28|32|36|38|64)px$/)
})
})
})
it('keeps conditional shared consumers opt-in instead of enabling other menus', () => {
expect(read('modules/loading-lists/components/LoadingListsView.vue')).toContain("'admin-density': isPreload")
expect(read('modules/loading-lists/components/PalletLabelPrintDialog.vue')).toContain("'admin-density-dialog': dense")
expect(read('modules/container-loading/components/ContainerSimulationCreate.vue')).toContain("'admin-density-dialog': dense")
for (const file of ['customers/pages/CustomersView.vue', 'customers/pages/OverseasStorageFeesView.vue', 'unified-orders/pages/UnifiedOrdersView.vue']) {
expect(read(`modules/${file}`), file).toContain('density-action')
}
})
})