Files
admin-web/src/styles/adminDensity.test.ts
T
2026-10-28 17:18:01 +08:00

1881 lines
125 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import ts from 'typescript'
import { describe, expect, it } from 'vitest'
import { compileStyle, parse } from 'vue/compiler-sfc'
import { adminNavigation, getNavigationLeaves } from '@/core/navigation/adminNavigation'
// Approved entry/consumer inventory. Do not discover unrelated menus recursively.
const cargoConsumers = [
'ocean-bookings/pages/OceanBookingsView.vue', 'ocean-bookings/pages/OceanBookingDetailView.vue',
'ocean-bookings/components/OceanBookingDocumentsPanel.vue', 'ocean-bookings/components/OceanBookingPrintDialog.vue',
'trucking/pages/TruckAppointmentsView.vue', 'trucking/pages/TruckAppointmentCreateView.vue',
'trucking/pages/TruckAppointmentDetailView.vue', 'trucking/components/TruckAppointmentForm.vue',
'trucking/components/TruckAttachmentDialog.vue', 'container-loading/pages/ContainersView.vue',
'container-loading/pages/ContainerDetailView.vue', 'container-loading/pages/SmartLoadingWorkbenchView.vue',
'container-loading/components/SmartLoadingDialog.vue', 'container-loading/pages/GlobalSmartLoadingView.vue',
'loading-lists/pages/PreloadPlanDetailView.vue',
'container-loading/components/OceanBookingLinkDialog.vue', 'container-loading/components/ContainerOverviewPanel.vue',
'port-schedules/components/ContainerPortSchedulePanel.vue',
'container-loading/components/ContainerCustomsPanel.vue', 'container-loading/components/ContainerLastLegFeesPanel.vue',
'loading-lists/components/ContainerFeesPanel.vue', 'port-schedules/components/CarrierScheduleQueryPanel.vue',
]
const overseasConsumers = [
'overseas-warehouse/pages/LastMileOrdersView.vue',
'overseas-warehouse/pages/LastMileOrderDetailView.vue',
'overseas-warehouse/pages/InboundOrdersView.vue',
'overseas-warehouse/pages/InboundOrderDetailView.vue',
'overseas-warehouse/pages/DispatchesView.vue',
'overseas-warehouse/pages/DispatchDetailView.vue',
'overseas-warehouse/components/TransportExecutionPanel.vue',
'overseas-warehouse/pages/OutboundsView.vue',
'overseas-warehouse/pages/OutboundDetailView.vue',
'overseas-warehouse/pages/DeliveryOrdersView.vue',
'overseas-warehouse/pages/DeliveryOrderDetailView.vue',
'overseas-warehouse/pages/RelabelOrdersView.vue',
'overseas-warehouse/pages/RelabelOrderDetailView.vue',
'overseas-warehouse/pages/PalletsView.vue',
'overseas-warehouse/pages/PalletDetailView.vue',
'overseas-warehouse/pages/LocationsView.vue',
'overseas-warehouse/pages/CargoDiscardsView.vue',
'overseas-warehouse/pages/InventoryView.vue',
'overseas-warehouse/pages/InventoryDetailView.vue',
'overseas-warehouse/pages/InventoryTransactionsView.vue',
'overseas-warehouse/pages/ReturnInventoryManagementView.vue',
'overseas-warehouse/components/WarehouseTransferReviewDialog.vue',
'overseas-warehouse/components/SmartDispatchDialog.vue',
'overseas-warehouse/components/DeliveryDocumentsDialog.vue',
]
const overseasBusinessConsumers = [
'overseas-business/pages/OverseasFeesView.vue',
'overseas-business/components/TransportDemandForm.vue',
'overseas-business/pages/PurchaseOrdersView.vue', 'overseas-business/pages/PurchaseOrderDetailView.vue',
'overseas-business/pages/PurchaseOrderEditView.vue', 'overseas-business/pages/TransportOrdersView.vue',
'overseas-business/pages/TruckOrderDetailView.vue',
'overseas-business/pages/TransportOrderFormView.vue', 'overseas-business/components/ProcurementNodeActions.vue', 'overseas-business/components/ProcurementPhysicalFields.vue', 'overseas-business/components/ProcurementTransportFields.vue', 'overseas-business/components/ProcurementFinanceFields.vue', 'overseas-business/components/ProcurementChargeFields.vue', 'overseas-business/components/ProcurementControlFields.vue',
]
const customerServiceConsumers = [
'overseas-warehouse/pages/ExceptionOrdersView.vue', 'overseas-warehouse/pages/CustomerVisitsView.vue',
]
const financeConsumers = [
'finance/pages/FinanceCustomerAccountsView.vue', 'finance/pages/FinanceCustomerArapView.vue',
'finance/pages/FinanceCustomerStatementsView.vue', 'finance/pages/FinanceFormalSettlementPage.vue',
'finance/pages/FinanceReceivableStatisticsView.vue',
]
const generalCatalogConsumers = [
'suppliers/pages/SuppliersView.vue',
'suppliers/pages/SupplierDetailView.vue',
'suppliers/pages/SupplierFormView.vue',
'suppliers/components/SupplierLicenseDialog.vue',
'overseas-master-data/pages/ExchangeRatesView.vue',
'overseas-master-data/pages/OverseasQuotesView.vue',
'overseas-master-data/pages/VehiclesView.vue',
'overseas-master-data/pages/VehicleDetailView.vue',
'overseas-master-data/components/TransportRouteDialog.vue',
'overseas-master-data/pages/DriversView.vue',
'overseas-master-data/pages/TripsView.vue',
'overseas-master-data/pages/CapacityOffersView.vue',
'overseas-master-data/pages/CapacityOfferDetailView.vue',
'overseas-master-data/components/CapacityInquiryDialog.vue',
'overseas-master-data/components/CapacityInquiryTable.vue',
'overseas-master-data/components/CapacityOfferSettingsDialog.vue',
'overseas-master-data/pages/TripOperationsView.vue',
'overseas-master-data/components/TripOperationsCharts.vue',
'overseas-master-data/pages/TripDetailView.vue',
'overseas-master-data/components/TripPlanDialog.vue',
'overseas-master-data/pages/DriverDetailView.vue',
'overseas-master-data/components/DriverEditorDialog.vue',
'overseas-master-data/components/DriverLicensePreview.vue',
'master-data/express-companies/pages/ExpressCompaniesView.vue',
'master-data/counterparties/pages/PartnersView.vue',
'fee-items/pages/FeeItemsView.vue',
'warehouses/pages/WarehousesView.vue',
'fba-pricing/carrier-rates/pages/CarrierRatesView.vue',
'fba-pricing/carrier-rates/pages/CarrierRateDetailView.vue',
'downloads/pages/DownloadCenterView.vue',
]
const notificationConsumers = [
'notifications/pages/PlatformNotificationsView.vue', 'notifications/pages/MessageManagementView.vue',
'notifications/pages/MessageTemplatesView.vue', 'notifications/components/MessageComposerDialog.vue',
'notifications/components/TemplateEditorDialog.vue',
]
const systemConsumers = ['iam/pages/InternalUsersView.vue', 'iam/pages/RolesView.vue', 'iam/pages/PermissionsView.vue']
const selfOperatedPages = ['self-operated/components/AnalyticsView.vue', 'self-operated/components/InventoryTurnoverView.vue']
const selfOperatedConsumers = [...selfOperatedPages, 'self-operated/components/SkuTrend.vue', 'self-operated/pages/OverviewView.vue']
const consumers = [
'overseas-business/components/OverseasFeeEditor.vue', 'overseas-business/components/StorageRentPanel.vue',
...selfOperatedConsumers,
'overseas-business/components/FleetArrangementSummary.vue',
...notificationConsumers,
...systemConsumers,
...generalCatalogConsumers,
...financeConsumers,
...customerServiceConsumers,
...overseasBusinessConsumers,
...overseasConsumers,
...cargoConsumers,
'customers/pages/CustomersView.vue', 'customers/pages/CustomerDetailView.vue',
'customers/pages/OverseasStorageFeesView.vue', 'customers/components/CustomerBillingRulesPanel.vue',
'unified-orders/components/InboundFeeDetailManager.vue', 'unified-orders/components/OrderDeclarationEditorDialog.vue',
'customers/CustomerOverseasStorageFeePanel.vue', 'unified-orders/pages/UnifiedOrdersView.vue',
'inbounds/core/DropshipPalletPrintPanel.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 = ['iam/iamDensity.css', 'finance/financeDensity.css', 'overseas-business/overseasBusinessDensity.css', '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' },
{ title: '船期日历', path: '/sailing-calendar', file: 'sailing-calendar/pages/SailingCalendarView.vue' },
]
const customerPages = [
{ title: '客户列表', path: '/customers', file: 'customers/pages/CustomersView.vue' },
{ title: '海外仓阶梯费用', path: '/customers/overseas-storage-fees', file: 'customers/pages/OverseasStorageFeesView.vue' },
]
const densityPages = [...new Set([
...selfOperatedPages,
...notificationConsumers.filter(file => file.includes('/pages/')),
...systemConsumers,
...generalCatalogConsumers.filter(file => file.includes('/pages/')),
...financeConsumers,
...customerServiceConsumers,
...overseasBusinessConsumers,
...overseasConsumers.filter(file => !file.endsWith('PalletDetailView.vue')),
...cargoConsumers.filter(file => file.includes('/pages/') || ['ContainerOverviewPanel.vue', 'ContainerCustomsPanel.vue', 'ContainerLastLegFeesPanel.vue', 'ContainerFeesPanel.vue', 'OceanBookingDocumentsPanel.vue'].some(name => file.endsWith(name))),
...domesticPages.map(page => page.file), 'loading-lists/pages/PreloadPlanDetailView.vue',
'loading-lists/components/LoadingListDetailDrawer.vue', 'domestic-containers/pages/DomesticContainerInventoryDetailView.vue',
...customerPages.map(page => page.file), 'customers/pages/CustomerDetailView.vue', 'audit/components/CustomerAuditLogsPanel.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, dense = preload, region = 'DOMESTIC', workbench = false, embedded = false): 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 []
// Keep visual opt-in and embedding independent from the shared component's density/region modes.
const value = new Function('mode', 'isPreload', 'dense', 'isDomestic', 'isOverseas', 'workbench', 'embedded', `return (${prop.exp.content})`)(preload ? 'preload' : 'confirmed', preload, dense, region === 'DOMESTIC', region === 'OVERSEAS', workbench, embedded) as Record<string, boolean>
const flatten = (value: unknown): string[] => typeof value === 'string' ? value.split(/\s+/) : Array.isArray(value) ? value.flatMap(flatten) : value && typeof value === 'object' ? Object.entries(value).filter(([, enabled]) => enabled).flatMap(([names]) => names.split(/\s+/)) : []
return flatten(value)
})
const elementsOf = (source: string, tag?: string): ElementNode[] => {
const parsed = parse(source)
expect(parsed.errors).toEqual([])
const result: ElementNode[] = []
const visit = (nodes: TemplateNode[]) => {
for (const node of nodes) {
if (node.type !== 1) continue
if (!tag || node.tag === tag) result.push(node)
visit(node.children)
}
}
visit(parsed.descriptor.template!.ast!.children)
return result
}
const expectWorkbenchDialogs = (source: string) => {
const dialogs = elementsOf(source, 'AppDialog')
expect(dialogs.length).toBeGreaterThan(0)
for (const dialog of dialogs) {
expect(classesOf(dialog), dialog.loc.source.split('>')[0]).toEqual(expect.arrayContaining(['admin-density-dialog', 'admin-workbench', 'is-dialog']))
}
return dialogs
}
const confirmationClasses = (source: string): string[][] => {
const script = source.includes('<script') ? parse(source).descriptor.scriptSetup!.content : source
const ast = ts.createSourceFile('confirmation.ts', script, ts.ScriptTarget.Latest, true)
const result: string[][] = []
const visit = (node: ts.Node) => {
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.expression.getText(ast) === 'ElMessageBox') {
const options = node.arguments[2]
expect(options && ts.isObjectLiteralExpression(options)).toBe(true)
const customClass = options && ts.isObjectLiteralExpression(options) ? options.properties.find(prop => ts.isPropertyAssignment(prop) && prop.name.getText(ast) === 'customClass') : undefined
expect(customClass && ts.isPropertyAssignment(customClass) && ts.isStringLiteral(customClass.initializer)).toBe(true)
if (customClass && ts.isPropertyAssignment(customClass) && ts.isStringLiteral(customClass.initializer)) result.push(customClass.initializer.text.split(/\s+/))
}
ts.forEachChild(node, visit)
}
visit(ast)
return result
}
const styles = [
...parse(read('shared/components/BatchImportDialog.vue')).descriptor.styles.map(style => ({ file: 'BatchImportDialog.vue', css: style.content })),
...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('draws only one separator between an Element table and pagination', () => {
const style = parse(read('shared/components/DataTable.vue')).descriptor.styles[0]!
const compiled = compileStyle({ source: style.content, filename: 'DataTable.vue', id: 'data-v-border-test', scoped: true })
expect(compiled.errors).toHaveLength(0)
const selectors: string[] = []
compiled.rawResult!.root.walkRules(rule => {
if (rule.selector.includes('.el-table + .data-table__pagination')) {
selectors.push(rule.selector)
expect(rule.nodes.some(node => node.type === 'decl' && node.prop === 'border-top' && node.value === '0')).toBe(true)
}
})
expect(selectors).toHaveLength(1)
const host = document.createElement('div')
host.innerHTML = `<div class="data-table"><div class="el-table"><div class="el-table__inner-wrapper"></div></div><footer id="standard" class="data-table__pagination" data-v-border-test></footer></div>
<div class="data-table"><section class="custom-results"></section><footer id="custom" class="data-table__pagination" data-v-border-test></footer></div>`
expect(host.querySelector('#standard')!.matches(selectors[0]!)).toBe(true)
expect(host.querySelector('#custom')!.matches(selectors[0]!)).toBe(false)
expect(style.content).not.toMatch(/inner-wrapper::before\s*\{[^}]*display:\s*none/)
})
it('keeps pallet and container overview metric appearance owned by shared primitives', () => {
for (const file of ['warehouse-resources/pallets/pages/PalletsView.vue', 'overseas-warehouse/pages/PalletsView.vue']) {
const page = parse(read(`modules/${file}`)).descriptor
const css = page.styles.map(style => style.content).join('\n')
expect(page.template!.content, file).toMatch(/<AdminPage\b[^>]*spacing="compact"/)
expect(css, file).toContain('.admin-workbench.pallet-page.admin-page.is-spacing-compact { gap: 0; }')
expect(page.template!.content, file).toContain('workbench-metric workbench-tone is-compact')
expect(css, file).not.toMatch(/\.summary-grid\s+(?:article|span|strong|\.idle|\.occupied)\b/)
expect(css, file).not.toMatch(/\.workbench-metric[\s.{]/)
}
const containerPage = parse(read('modules/domestic-containers/pages/DomesticContainersView.vue')).descriptor
const containerStyles = containerPage.styles.map(style => style.content).join('\n')
+ read('modules/domestic-containers/pages/domesticContainersSummary.css')
expect(containerPage.template!.content).toMatch(/<AdminPage\b[^>]*spacing="compact"/)
expect(containerStyles).toContain('.admin-workbench.domestic-containers-page.admin-page.is-spacing-compact { gap: 0; }')
expect(containerPage.template!.content).toContain('workbench-metric workbench-tone is-compact')
expect(containerStyles).not.toMatch(/\.inventory-summary\s*(?:>|\s)\s*(?:article|div|span|strong)\b/)
expect(containerStyles).not.toMatch(/\.workbench-metric[\s.{]/)
})
it('lets local transport editors fill the workspace and keeps form density in shared primitives', () => {
const editor = read('modules/overseas-business/pages/TransportOrderFormView.vue')
const demand = parse(read('modules/overseas-business/components/TransportDemandForm.vue')).descriptor
const css = demand.styles.map(style => style.content).join('\n')
const layout = read('modules/overseas-business/overseasBusinessDensity.css')
expect(editor).toContain('overseas-transport-editor')
expect(editor).toContain('transport-customer-section density-form-section')
expect(layout).toMatch(/\.admin-density\.overseas-business-editor\s*\{\s*width: 100%;\s*\}/)
expect(layout).not.toContain('max-width: 1120px')
expect(demand.template!.content).toContain('admin-density admin-workbench density-form')
expect(demand.template!.content.match(/<FormSection class="density-form-section"/g)).toHaveLength(3)
expect(css).not.toMatch(/max-width|--el-component-size|font-size|el-form-item__label|margin-bottom/)
expect(css).toContain('repeat(2, minmax(0, 1fr))')
expect(css).toContain('repeat(3, minmax(0, 1fr))')
expect(css).toContain('repeat(4, minmax(0, 1fr))')
})
it('separates shared list filters with the same 5px radius across density themes', () => {
const declarations = (source: string, selector: string) => {
const values: Record<string, string> = {}
cssRoot(source).walkRules(rule => {
if (rule.selector === selector) rule.walkDecls(decl => { values[decl.prop] = decl.value })
})
return values
}
const foundation = declarations(read('styles/foundation.css'), ':root')
expect(foundation['--cc-list-query-radius']).toBe('5px')
expect(foundation['--cc-list-query-gap']).toBe('16px')
expect(foundation['--cc-list-table-header-bg']).toBe('#edf4ff')
expect(foundation['--cc-list-table-header-text']).toBe('#284b78')
expect(declarations(shared, ':is(.admin-density, .admin-density-dialog, .admin-density-drawer) .el-table th.el-table__cell')).toEqual({
background: 'var(--cc-list-table-header-bg)',
color: 'var(--cc-list-table-header-text)',
})
const table = parse(read('shared/components/DataTable.vue')).descriptor.styles.map(style => style.content).join('\n')
expect(declarations(table, '.data-table > .data-table__filters')).toMatchObject({
'margin-bottom': 'var(--cc-list-query-gap, 16px)',
'border-radius': 'var(--cc-list-query-radius, 5px)',
border: '1px solid var(--list-card-border, var(--cc-border))',
})
const headerSelector = '.data-table > .data-table__toolbar:has(+ .data-table__filters)'
expect(declarations(table, headerSelector)['border-bottom']).toBe('0')
const fixture = document.createElement('div')
fixture.innerHTML = '<div class="data-table"><div id="with-filters" class="data-table__toolbar"></div><div class="data-table__filters"></div></div><div class="data-table"><div id="without-filters" class="data-table__toolbar"></div><div class="el-table"></div></div>'
expect(fixture.querySelector('#with-filters')!.matches(headerSelector)).toBe(true)
expect(fixture.querySelector('#without-filters')!.matches(headerSelector)).toBe(false)
expect(declarations(shared, '.admin-density .query-toolbar')['border-radius']).toBe('var(--cc-list-query-radius, 5px)')
expect(declarations(shared, '.admin-density .surface-panel')['border-radius']).toBe('var(--admin-radius, 4px)')
const query = parse(read('shared/components/QueryToolbar.vue')).descriptor.styles.map(style => style.content).join('\n')
expect(declarations(query, '.query-toolbar')['border-radius']).toBe('var(--cc-list-query-radius, 5px)')
})
it('keeps sailing cutoff controls compact and delegates editor borders to the shared opt-in', () => {
const view = parse(read('modules/sailing-calendar/pages/SailingCalendarView.vue')).descriptor
const rules = new Map<string, Record<string, string>>()
for (const style of view.styles) cssRoot(style.content).walkRules(rule => {
const values: Record<string, string> = {}
rule.walkDecls(decl => { values[decl.prop] = decl.value })
rules.set(rule.selector, values)
})
expect(rules.get('.sailing-entry-label')).toEqual({ width: '116px' })
expect(rules.get('.cutoff-presets')).toEqual({ flex: '0 0 auto' })
expect(rules.get('.sailing-date-editor')).toMatchObject({ width: '280px', 'max-width': '100%' })
expect(rules.get('.cutoff-rule-row, .cutoff-time-row')).toMatchObject({ 'flex-wrap': 'wrap' })
expect(view.template!.content).toContain('density-table-editor density-table-editor--outlined cutoff-days-editor')
expect(view.template!.content).toContain('density-table-editor--outlined cutoff-time-editor')
expect(view.template!.content).toContain('role="status" aria-live="polite"')
expect(view.styles.map(style => style.content).join('\n')).not.toContain('--el-input-border-color')
})
it('outlines opted-in customer forms without overriding native focus, validation or disabled styles', () => {
const rules = new Map<string, Record<string, string>>()
cssRoot(shared).walkRules(rule => {
const values: Record<string, string> = {}
rule.walkDecls(decl => { values[decl.prop] = decl.value })
rules.set(rule.selector, values)
})
expect(rules.get(':is(.admin-density, .admin-density-dialog) .density-form--outlined :is(.el-input, .el-textarea)')).toEqual({
'--el-input-border-color': 'var(--el-border-color)',
'--el-input-hover-border-color': 'var(--el-border-color-hover)',
'--el-input-bg-color': 'var(--admin-surface)',
})
expect(rules.get(':is(.admin-density, .admin-density-dialog) .density-form--outlined')).toEqual({
'--el-border-radius-base': 'var(--cc-radius-lg)',
})
const customerRules = new Map<string, Record<string, string>>()
cssRoot(read('modules/customers/customerDensity.css')).walkRules(rule => {
const values: Record<string, string> = {}
rule.walkDecls(decl => { values[decl.prop] = decl.value })
customerRules.set(rule.selector, values)
})
expect(customerRules.get('.customer-editor-dialog .density-form--outlined .el-input-number .el-input__inner')).toEqual({ 'text-align': 'left' })
})
it('outlines only the three opted-in customer billing editors without overriding native focus or disabled styles', () => {
const rules = new Map<string, Record<string, string>>()
cssRoot(shared).walkRules(rule => {
const values: Record<string, string> = {}
rule.walkDecls(decl => { values[decl.prop] = decl.value })
rules.set(rule.selector, values)
})
expect(rules.get(':is(.admin-density, .admin-density-dialog) .density-table--editable .density-table-editor--outlined .el-input')).toEqual({
'--el-input-border-color': 'var(--el-color-primary-light-5)',
'--el-input-hover-border-color': 'var(--el-color-primary)',
'--el-input-bg-color': 'var(--admin-surface)',
})
const ast = parse(read('modules/unified-orders/pages/UnifiedOrdersView.vue')).descriptor.template!.ast!
const inputIds: string[] = []
const visit = (nodes: typeof ast.children) => { for (const node of nodes) {
if (node.type !== 1) continue
if (classesOf(node).includes('density-table-editor--outlined')) {
const input = node.children.find(child => child.type === 1 && child.tag === 'el-input-number') as ElementNode | undefined
expect(input).toBeDefined()
const id = input!.props.find(prop => prop.type === 6 && prop.name === 'id')
inputIds.push(id?.type === 6 ? id.value?.content || '' : '')
}
visit(node.children)
} }
visit(ast.children)
expect(inputIds).toEqual(['fee-confirm-weight', 'fee-confirm-volume', 'fee-confirm-unit-price'])
})
it('allows container filters and their final action group to wrap by available width', () => {
const source = read('modules/container-loading/pages/ContainersView.vue')
for (const style of parse(source).descriptor.styles) {
cssRoot(style.content).walkRules(rule => {
if (!rule.selector.includes('.query-toolbar__tail')) return
rule.walkDecls(decl => {
if (decl.prop === 'width') expect(decl.value).not.toBe('100%')
if (decl.prop === 'flex') expect(decl.value).not.toBe('0 0 100%')
})
})
}
})
it('keeps declaration order fields outlined within the continuous cell-editor layout', () => {
const source = read('modules/unified-orders/components/OrderDeclarationEditorDialog.vue')
const orderFields = source.slice(source.indexOf('<el-form label-position="left"'), source.indexOf('</el-form>') + '</el-form>'.length)
expect(orderFields).toContain('class="order-fields density-form density-form--outlined"')
expect(orderFields).toContain('density-table density-table--editable')
expect(orderFields).not.toMatch(/<el-(?:row|col)\b/)
expect(orderFields).toContain('<span aria-label="渠道编码">')
expect(orderFields).not.toContain('<el-input :model-value="form.channelCode"')
})
it('shares the measurement palette through explicit dialog sections without changing other dialog bodies', () => {
const tokenValues: Record<string, string> = {}
cssRoot(tokens).walkDecls(decl => { tokenValues[decl.prop] = decl.value })
const rules = new Map<string, Record<string, string>>()
cssRoot(shared).walkRules(rule => {
const values: Record<string, string> = {}
rule.walkDecls(decl => { values[decl.prop] = decl.value })
rules.set(rule.selector, values)
})
for (const [tone, color] of [['summary', '#f4fbf8'], ['source', '#f7f9ff'], ['work', '#fffbf4']]) {
expect(tokenValues[`--admin-dialog-${tone}-bg`]).toBe(color)
expect(rules.get(`:is(.admin-density-dialog, .admin-density-drawer:where(.admin-density-drawer--detail, .admin-density-drawer--editor)) .density-dialog-section[data-tone="${tone}"]`)).toMatchObject({
background: `var(--admin-dialog-${tone}-bg)`,
'border-color': `var(--admin-dialog-${tone}-border)`,
})
}
expect(rules.get(':is(.admin-density-dialog, .admin-density-drawer:where(.admin-density-drawer--detail, .admin-density-drawer--editor)) .density-dialog-section')).not.toHaveProperty('height')
expect(rules.get(':is(.admin-density-dialog, .admin-density-drawer:where(.admin-density-drawer--detail, .admin-density-drawer--editor)) .density-dialog-note')).toMatchObject({ color: 'var(--cc-muted)', 'font-size': 'var(--admin-font-caption)' })
expect(rules.get(':is(.admin-density-dialog, .admin-density-drawer:where(.admin-density-drawer--editor)) .density-actions--footer')).toMatchObject({ 'justify-content': 'flex-end', 'flex-wrap': 'wrap', gap: 'var(--admin-space-3)' })
for (const [selector, declarations] of rules) {
if (selector.includes('density-dialog-section')) expect(Object.keys(declarations).filter(name => name.startsWith('border-left')), selector).toEqual([])
}
const editorRule = rules.get(':is(.admin-density, .admin-density-dialog) .density-table--editable :is(.el-input, .el-textarea)')
expect(editorRule).toMatchObject({ '--el-input-border-color': 'transparent', '--el-input-hover-border-color': 'var(--admin-table-border)' })
expect(editorRule).not.toHaveProperty('--el-input-focus-border-color')
expect(rules.get(':is(.admin-density, .admin-density-dialog) .density-table--editable .el-form-item')).toMatchObject({ 'margin-bottom': '0' })
expect(rules.get(':is(.admin-density, .admin-density-dialog) .density-table--editable .el-form-item.is-error')).toMatchObject({ 'margin-bottom': 'var(--admin-space-5)' })
for (const selector of rules.keys()) if (selector.includes('density-dialog-section')) expect(selector.startsWith(':is(.admin-density-dialog, .admin-density-drawer:where(.admin-density-drawer--detail, .admin-density-drawer--editor)) ')).toBe(true)
const measured = read('modules/inbounds/measurement/measurementDensity.css')
expect(measured).not.toMatch(/border-left(?:-color|-width)?\s*:/)
for (const color of ['#f4fbf8', '#f7f9ff', '#fffbf4']) expect(measured).not.toContain(color)
for (const file of ['unified-orders/pages/UnifiedOrdersView.vue', 'unified-orders/components/OrderDeclarationEditorDialog.vue', 'unified-orders/components/InboundFeeDetailManager.vue', 'inbounds/measurement/InboundMeasurementPanel.vue', 'inbounds/weight/components/WeightOcrPanel.vue', 'domestic-containers/pages/DomesticContainersView.vue', 'sailing-calendar/pages/SailingCalendarView.vue', 'ocean-bookings/components/OceanBookingPrintDialog.vue', 'ocean-bookings/pages/OceanBookingDetailView.vue', 'trucking/components/TruckAttachmentDialog.vue', 'container-loading/pages/ContainersView.vue', 'container-loading/pages/ContainerDetailView.vue', 'container-loading/components/OceanBookingLinkDialog.vue']) {
const source = read('modules/' + file)
expect(parse(source).errors, file).toEqual([])
const ast = parse(source).descriptor.template!.ast!
const visit = (nodes: typeof ast.children, inDialog = false) => { for (const node of nodes) {
if (node.type !== 1) continue
const inside = inDialog || ['AppDialog', 'el-dialog'].includes(node.tag)
if (classesOf(node).some(name => ['density-dialog-section', 'density-table--editable'].includes(name))) expect(inside, file + ': sections and cell editors belong to an explicit dialog').toBe(true)
visit(node.children, inside)
} }
visit(ast.children)
}
})
it('opts the audit detail drawer into the same palette without styling unselected drawers or clipping snapshots', () => {
const rules = new Map<string, Record<string, string>>()
cssRoot(shared).walkRules(rule => {
const declarations: Record<string, string> = {}
rule.walkDecls(decl => { declarations[decl.prop] = decl.value })
rules.set(rule.selector, declarations)
})
const snapshot = rules.get(':is(.admin-density-dialog, .admin-density-drawer:where(.admin-density-drawer--detail, .admin-density-drawer--editor)) .density-code-block')!
expect(snapshot).toMatchObject({ 'white-space': 'pre-wrap', 'overflow-wrap': 'anywhere' })
for (const key of ['height', 'min-height', 'max-height', 'overflow', 'overflow-y']) expect(snapshot).not.toHaveProperty(key)
for (const selector of rules.keys()) {
if (selector.includes('admin-density-drawer') && (selector.includes('density-dialog-section') || selector.includes('density-table'))) expect(selector).toContain('.admin-density-drawer:where(.admin-density-drawer--detail, .admin-density-drawer--editor)')
if (selector.includes('.el-drawer__header') || selector.includes('.el-drawer__body')) expect(selector).not.toContain('admin-density-drawer--detail')
}
const audit = parse(read('modules/audit/components/CustomerAuditLogsPanel.vue')).descriptor
const ast = audit.template!.ast!
const visit = (nodes: typeof ast.children, inside = false) => {
for (const node of nodes) {
if (node.type !== 1) continue
const inDrawer = inside || (node.tag === 'AppDrawer' && classesOf(node).includes('admin-density-drawer--detail') && classesOf(node).includes('admin-density-drawer'))
if (classesOf(node).some(value => ['density-dialog-section', 'density-table', 'density-code-block'].includes(value))) expect(inDrawer).toBe(true)
visit(node.children, inDrawer)
}
}
visit(audit.template!.ast!.children)
for (const style of audit.styles) expect(style.content).not.toMatch(/snapshot[^{}]*\{[^}]*max-height|border-left:/)
})
it('keeps review presentation in the shared dialog and resource editors on the shared form scale', () => {
const review = parse(read('modules/inbounds/review/MeasurementReviewPanel.vue')).descriptor
for (const style of review.styles) {
expect(style.content).not.toContain('linear-gradient')
expect(style.content).not.toMatch(/box-shadow:\s*inset/)
}
const host = parse(read('modules/inbounds/components/InboundTaskList.vue')).descriptor.template!.ast!
let reviewHostFound = false
const visit = (nodes: typeof host.children) => { for (const node of nodes) {
if (node.type !== 1) continue
if (classesOf(node).includes('inbound-task-dialog')) {
expect(node.tag).toBe('AppDialog')
expect(classesOf(node)).toContain('admin-density-dialog')
expect(node.props.some(prop => prop.type === 6 && prop.name === 'fullscreenable')).toBe(true)
expect(node.children.some(child => child.type === 1 && child.props.some(prop => prop.type === 7 && prop.name === 'slot' && prop.arg?.type === 4 && prop.arg.content === 'footer'))).toBe(true)
reviewHostFound = true
}
visit(node.children)
} }
visit(host.children)
expect(reviewHostFound).toBe(true)
for (const file of ['locations/pages/LocationsView.vue', 'pallets/pages/PalletsView.vue']) {
const source = read('modules/warehouse-resources/' + file)
expect(source).toContain('class="editor-form density-form density-form--flush-end"')
for (const style of parse(source).descriptor.styles) {
expect(style.content).not.toContain('.editor-form :deep(.el-form-item__label)')
expect(style.content).not.toContain('.editor-form :deep(.el-form-item)')
}
}
expect(shared).toContain('.admin-density-dialog .density-form--flush-end > .el-form-item:last-child')
expect(shared).not.toContain('.admin-density-dialog .density-form > .el-form-item:last-child')
})
it('keeps batch approval overflow tooltips single-line without forcing its headers or error tags to truncate', () => {
const view = parse(read('modules/unified-orders/pages/UnifiedOrdersView.vue')).descriptor.template!.ast!
const tooltipColumns: string[] = []
const visit = (nodes: typeof view.children, inBatch = false) => { for (const node of nodes) {
if (node.type !== 1) continue
const inside = inBatch || classesOf(node).includes('batch-approval-table')
if (inside && node.tag === 'el-table-column') {
const label = node.props.find(prop => prop.type === 6 && prop.name === 'label')
if (label?.type === 6 && ['客户', '中文品名'].includes(label.value?.content || '')) {
expect(node.props.some(prop => prop.type === 6 && prop.name === 'show-overflow-tooltip')).toBe(true)
tooltipColumns.push(label.value!.content)
}
}
visit(node.children, inside)
} }
visit(view.children)
expect(tooltipColumns).toEqual(['客户', '中文品名'])
const rules = new Map<string, Record<string, string>>()
cssRoot(read('modules/unified-orders/styles/orderDensity.css')).walkRules(rule => {
const declarations: Record<string, string> = {}
rule.walkDecls(decl => { declarations[decl.prop] = decl.value })
rules.set(rule.selector, declarations)
})
expect(rules.has('.admin-density-dialog.el-dialog .batch-approval-table .cell')).toBe(false)
expect(rules.get('.admin-density-dialog.el-dialog .batch-approval-table td .cell.el-tooltip')).toMatchObject({
'white-space': 'nowrap', 'text-overflow': 'ellipsis', overflow: 'hidden', 'overflow-wrap': 'normal',
})
expect(rules.get('.admin-density-dialog.el-dialog .batch-approval-table .el-tag')?.['white-space']).toBe('normal')
})
it('opts transfer approval into the shared centered confirmation appearance', () => {
const dialog = read('modules/overseas-warehouse/components/WarehouseTransferReviewDialog.vue')
const css = read('styles/adminDensity.css')
const [frame] = expectWorkbenchDialogs(dialog)
expect(classesOf(frame!)).toContain('density-confirm-dialog')
expect(dialog).toContain('align-center')
expect(css).toContain('.admin-density-dialog.density-confirm-dialog.el-dialog:not(.is-fullscreen) { margin: auto; }')
expect(css).toContain('.admin-density-dialog.density-confirm-dialog.el-dialog > .el-dialog__header { border-bottom: 0; }')
expect(css).toContain('.admin-density-dialog.density-confirm-dialog.el-dialog > .el-dialog__footer { border-top: 0;')
})
it('keeps the simplified calendar in two columns with bounded previews and scrollable daily details', () => {
const view = parse(read('modules/appointment-calendar/AppointmentCalendarView.vue')).descriptor
const panel = parse(read('modules/appointment-calendar/AppointmentCalendarPanel.vue')).descriptor
expect(view.template!.content).toContain('<AppointmentCalendarPanel')
expect(view.scriptSetup!.content).not.toMatch(/getWorkSummary|getWorkCalendar|getWorkItems/)
const declarations = (selector: string) => {
const values: Record<string, string> = {}
cssRoot(panel.styles.map(style => style.content).join('\n')).walkRules(selector, rule => {
if (rule.parent?.type !== 'root') return
rule.walkDecls(decl => { values[decl.prop] = decl.value })
})
return values
}
expect(declarations('.appointment-calendar')['grid-template-columns']).toBe('minmax(0, 1fr) 360px')
expect(declarations('.calendar-day').height).toBe('146px')
expect(declarations('.week-grid .calendar-day').height).toBe('270px')
expect(declarations('.detail-events')['overflow-y']).toBe('auto')
expect(declarations('.detail-event dd')['overflow-wrap']).toBe('anywhere')
expect(panel.template!.content).toContain('.slice(0, 3)')
expect(panel.template!.content).toContain('v-if="canCreate"')
expect(panel.template!.content).toContain('新增预约')
})
it('keeps calendar creation, its two consumers and arrival editing on shared dialog sections', () => {
const view = parse(read('modules/appointment-calendar/AppointmentCalendarView.vue')).descriptor
const create = parse(read('modules/appointment-calendar/AppointmentCreatePanel.vue')).descriptor
const work = parse(read('modules/appointment-calendar/WorkCreateAppointmentForm.vue')).descriptor
expect(view.scriptSetup!.content).toContain("import '@/styles/adminDensity.css'")
const dialogs = view.template!.content.match(/<AppDialog\b[^>]+>/g)!
expect(dialogs).toHaveLength(3)
for (const dialog of dialogs) { expect(dialog).toContain('admin-density-dialog'); expect(dialog).toContain('fullscreenable') }
expect(view.template!.content).toContain('density-table density-table--editable')
expect(view.template!.content).toContain('label-position="left" label-width="150px"')
expect(view.template!.content).toContain('<el-date-picker v-model="plannedAt" type="datetime"')
expect(view.styles.map(style => style.content).join('\n')).not.toContain('arrival-field')
expect(create.template!.content).toContain('aria-label="预约保存回执"')
expect(create.template!.content).not.toContain('<FbaAppointmentFields')
expect(create.template!.content).toContain('<LineClampText :content="row.customerName"')
expect(create.template!.content).toContain('data-tone="source"')
expect(work.template!.content).toContain('<FbaAppointmentFields continuous')
expect(work.template!.content).toContain('density-actions density-actions--footer')
for (const source of [create.template!.content, read('modules/appointment-calendar/WorkDetailPanel.vue')]) expect(source).toContain('<WorkCreateAppointmentForm')
expect(create.styles).toHaveLength(0); expect(work.styles).toHaveLength(0)
})
it('shares list status and category tab styles across registered list consumers', () => {
for (const file of [
'unified-orders/pages/UnifiedOrdersView.vue', 'overseas-warehouse/pages/LastMileOrdersView.vue',
'overseas-master-data/pages/VehiclesView.vue', 'suppliers/pages/SuppliersView.vue',
'domestic-containers/pages/DomesticContainersView.vue', 'container-loading/pages/ContainersView.vue',
'appointment-calendar/AppointmentCargoTabs.vue',
]) {
const page = parse(read(`modules/${file}`)).descriptor
expect(page.template!.content).toMatch(/class="[^"]*density-status-tabs[^"]*" role="tablist"/)
expect(page.styles.map((style) => style.content).join('\n')).not.toMatch(/\.(status-tabs?|supplier-tabs|container-scope-tabs)\s*[{:.]/)
}
const returnTabs = elementsOf(read('modules/overseas-warehouse/pages/ReturnInventoryManagementView.vue'), 'el-tabs')
expect(returnTabs.some(node => ['density-element-tabs', 'density-status-tabs'].every(name => classesOf(node).includes(name)))).toBe(true)
const selectors: string[] = []
cssRoot(shared).walkRules((rule) => { selectors.push(...rule.selectors) })
expect(selectors).toContain('.admin-density .density-status-tabs > button.is-active::after')
expect(selectors).toContain('.admin-density .density-status-tabs > button:focus-visible')
const element = '.admin-density .density-element-tabs.density-status-tabs > .el-tabs__header .el-tabs__nav .el-tabs__item'
const rules: string[][] = []
cssRoot(shared).walkRules((rule) => { rules.push(rule.selectors) })
for (const suffix of ['', '::after', ':hover', ':focus-visible', '.is-active', '.is-active::after']) {
expect(rules.some((rule) => rule.includes('.admin-density .density-status-tabs > button' + suffix) && rule.includes(element + suffix))).toBe(true)
}
})
it('keeps dashboard visualizations outside detail-field table styling', () => {
const dashboard = read('modules/dashboard/pages/DashboardView.vue')
expect(dashboard).toContain('<AdminPage as="div" spacing="compact" class="admin-density admin-workbench">')
expect(dashboard).toContain('.dashboard-view > .admin-page.is-spacing-compact { gap: var(--admin-space-2); }')
const metrics = elementsOf(dashboard, 'div').filter(node => classesOf(node).includes('dashboard-metrics'))
expect(metrics).toHaveLength(1)
const cards = elementsOf(dashboard, 'article').filter(node => classesOf(node).includes('dashboard-metric'))
expect(cards).toHaveLength(1)
expect(classesOf(cards[0]!)).toEqual(expect.arrayContaining(['workbench-metric', 'workbench-tone']))
const icon = elementsOf(dashboard, 'span').find(node => classesOf(node).includes('workbench-icon'))
expect(icon).toBeDefined()
expect(icon!.children.some(node => node.type === 1 && node.tag === 'component')).toBe(true)
expect(dashboard).not.toContain('density-fields')
})
it('registers the self-operated dashboard with an explicit density root and chart layout', () => {
const source = read('modules/self-operated/pages/OverviewView.vue')
expect(source).toContain("@/styles/adminDensity.css")
expect(source).toContain('<AdminPage class="admin-density admin-workbench dashboard-page">')
expect(source).toContain('class="dashboard-kpis"')
expect(source).toContain('class="dashboard-main-charts"')
expect(source).not.toContain('density-fields')
expect(getNavigationLeaves(adminNavigation).map(node => node.path)).toContain('/self-operated/overview')
})
it('opts application-shell confirmations in even before a business page loads', () => {
const entries = ['app/layouts/AppLayout.vue', 'app/components/WorkspaceTabs.vue', 'app/router/index.ts', 'main.ts']
let count = 0
for (const file of entries) {
const source = read(file)
expect(source, file).toContain("@/styles/adminDensity.css")
const script = file.endsWith('.vue') ? parse(source).descriptor.scriptSetup!.content : source
const tree = ts.createSourceFile(file, script, ts.ScriptTarget.Latest, true)
const visit = (node: ts.Node) => {
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.expression.getText(tree) === 'ElMessageBox') {
count += 1
const options = node.arguments[2]
expect(options && ts.isObjectLiteralExpression(options), file).toBe(true)
const customClass = options && ts.isObjectLiteralExpression(options) ? options.properties.find(prop => ts.isPropertyAssignment(prop) && prop.name.getText(tree) === 'customClass') : undefined
expect(customClass && ts.isPropertyAssignment(customClass) && ts.isStringLiteral(customClass.initializer) && customClass.initializer.text, file).toBe('admin-density-confirm')
}
ts.forEachChild(node, visit)
}
visit(tree)
}
expect(count).toBe(5)
})
it('checks dialog roots along actual static and dynamic Vue imports from approved consumers', () => {
const pending = consumers.map(file => resolve(process.cwd(), 'src/modules', file))
const seen = new Set<string>()
while (pending.length) {
const file = pending.shift()!
if (seen.has(file)) continue
seen.add(file)
const descriptor = parse(readFileSync(file, 'utf8')).descriptor
const enqueue = (reference: string) => {
if (!reference.endsWith('.vue')) return
if (reference.startsWith('@/')) pending.push(resolve(process.cwd(), 'src', reference.slice(2)))
else if (reference.startsWith('.')) pending.push(resolve(dirname(file), reference))
}
for (const script of [descriptor.script, descriptor.scriptSetup]) {
if (!script) continue
const ast = ts.createSourceFile(file, script.content, ts.ScriptTarget.Latest, true)
const visitImports = (node: ts.Node) => {
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.expression.getText(ast) === 'ElMessageBox') {
expect(node.arguments[2]?.getText(ast), file + ': reachable confirmation').toContain('admin-density-confirm')
}
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) enqueue(node.moduleSpecifier.text)
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
const reference = node.arguments[0]
if (reference && ts.isStringLiteral(reference)) enqueue(reference.text)
}
ts.forEachChild(node, visitImports)
}
visitImports(ast)
}
if (['AppDialog.vue', 'AppDrawer.vue', 'BatchImportDialog.vue'].some(name => file.endsWith(name))) continue
const visitTemplate = (nodes: TemplateNode[]) => {
for (const node of nodes) {
if (node.type !== 1) continue
if (['AppDialog', 'el-dialog', 'BatchImportDialog'].includes(node.tag)) expect(classesOf(node), `${file}: reachable modal`).toContain('admin-density-dialog')
if (['AppDrawer', 'el-drawer'].includes(node.tag)) expect(classesOf(node), `${file}: reachable drawer`).toContain('admin-density-drawer')
visitTemplate(node.children)
}
}
if (descriptor.template?.ast) visitTemplate(descriptor.template.ast.children)
}
})
it('keeps warehouse editors and import results on shared sections and cell editors', () => {
const source = read('modules/warehouses/pages/WarehousesView.vue')
expect(source).toContain('class="density-form density-form--outlined density-dialog-sections"')
expect(source).toContain('density-table density-table--editable warehouse-editor-table')
const importer = expectWorkbenchDialogs(source).find(node => classesOf(node).includes('warehouse-import-dialog'))
expect(importer).toBeDefined()
const editor = expectWorkbenchDialogs(source).find(node => classesOf(node).includes('warehouse-editor-dialog'))
expect(editor).toBeDefined()
expect(editor!.loc.source).not.toContain('data-tone=')
expect(source).toContain('class="density-table warehouse-import-summary"')
expect(source).toContain('class="density-actions density-actions--footer"')
expect(source).not.toContain('<FormSection')
expect(source).not.toContain('<el-dialog')
expect(source).not.toContain(':global(.warehouse-editor-dialog .el-dialog__body)')
})
it('keeps role and permission editors on shared sections with one body scroll and fixed codes', () => {
const role = read('modules/iam/pages/RolesView.vue')
const permission = read('modules/iam/pages/PermissionsView.vue')
for (const source of [role, permission]) {
expect(source).toContain('fullscreenable :confirm-loading="saving"')
expect(source).toContain('density-table--editable')
expect(source).toContain('density-dialog-section')
expect(source).not.toContain('<FormSection')
expect(source).not.toContain('label-position="top"')
const css = parse(source).descriptor.styles.map(style => style.content).join('')
expect(css).not.toContain('el-dialog__body')
expect(css).not.toContain('border-left')
}
expect(role).toContain('data-tone="source"')
expect(role).not.toContain('max-height: 380px')
expect(role).toContain('.role-editor-layout { grid-template-columns: minmax(0, 1fr); }')
expect(permission).not.toContain('class="form-callout"')
})
it('keeps internal user forms on shared cells and native fullscreen with original geometry', () => {
const source = read('modules/iam/pages/InternalUsersView.vue')
expect(source).toContain('class="density-form density-form--outlined density-dialog-sections"')
expect(source).toContain('class="density-table density-table--editable user-editor-table"')
expect(source).toContain('class="density-dialog-section workbench-evidence"')
expect(source).toContain('fullscreenable destroy-on-close')
expect(source).toContain('width="min(650px, calc(100vw - 28px))"')
expect(source).not.toContain('<FormSection')
expect(source).not.toContain('label-position="top"')
const css = parse(source).descriptor.styles.map(style => style.content).join('')
expect(css).not.toMatch(/font-size|background|border|el-dialog__body|el-dialog__footer/)
expect(css).toContain(':not(.is-fullscreen)')
})
it('opts fee item editing into white dialog sections and existing cell editors', () => {
const source = read('modules/fee-items/pages/FeeItemsView.vue')
const ast = parse(source).descriptor.template!.ast!
let editorFound = false
const visit = (nodes: typeof ast.children, inEditor = false) => { for (const node of nodes) {
if (node.type !== 1) continue
const inside = inEditor || node.tag === 'AppDialog' && classesOf(node).includes('fee-item-editor')
if (node.tag === 'AppDialog' && classesOf(node).includes('fee-item-editor')) {
editorFound = true
for (const name of ['admin-density-dialog', 'fee-item-editor']) expect(classesOf(node)).toContain(name)
}
if (classesOf(node).some(name => ['density-dialog-section', 'density-table--editable'].includes(name))) expect(inside).toBe(true)
visit(node.children, inside)
} }
visit(ast.children)
expect(editorFound).toBe(true)
expect(source).not.toMatch(/density-dialog-section[^>]*data-tone=/)
expect(source).not.toContain('AppDrawer')
expect(source).toContain('class="density-form density-form--outlined density-dialog-sections"')
expect(source).toContain('label="默认价格(CNY)" label-width="120px"')
expect(source).not.toContain('<FormSection')
expect(source).not.toContain('label-position="top"')
expect(source).toContain('class="density-actions density-actions--footer"')
expect(source).toContain('width="min(760px, 94vw)" align-center')
expect(shared).toContain(':where(.admin-density-drawer--detail, .admin-density-drawer--editor)')
const css = parse(source).descriptor.styles.map(style => style.content).join('')
expect(css).not.toContain('direction-grid')
expect(css).not.toContain('delete-message')
})
it('keeps download task detail on shared facts, error and footer primitives', () => {
const source = read('modules/downloads/pages/DownloadCenterView.vue')
const dialog = source.slice(source.indexOf('<AppDialog'), source.indexOf('</AppDialog>'))
expectWorkbenchDialogs(source)
expect(dialog).toContain('fullscreenable')
expect(dialog).toContain('data-tone="summary"')
expect(dialog).toContain('class="task-detail-grid density-fields"')
expect(dialog).toContain('<LoadErrorState v-if="detailError" compact')
expect(dialog).toContain('class="density-actions density-actions--footer"')
expect(dialog).not.toContain('requestedUsername || detailTask.requestedBy')
const css = parse(source).descriptor.styles.map(style => style.content).join('')
expect(css).not.toMatch(/\.task-(?:detail-summary|failure)\b|\.detail-load-error\b/)
})
it('uses compact pallet-style cards above the customer visit list', () => {
const source = read('modules/overseas-warehouse/pages/CustomerVisitsView.vue')
expect(source.match(/class="workbench-metric workbench-tone is-compact"/g)).toHaveLength(3)
expect(source.indexOf('aria-label="回访统计"')).toBeLessThan(source.indexOf('<ListPageLayout'))
expect(source).not.toContain('visit-summary density-detail-overview')
expect(source).toContain('.admin-workbench.customer-visits-page.admin-page.is-spacing-compact { gap: 0; }')
for (const field of ['visitable', 'pending', 'completed']) expect(source).toContain('summary.' + field + ".toLocaleString('zh-CN')")
})
it('keeps ship price book upload compact with shared table and confirmation styles', () => {
const list = read('modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue')
const detail = read('modules/fba-pricing/carrier-rates/pages/CarrierRateDetailView.vue')
const dialog = list.slice(list.indexOf('<AppDialog'), list.indexOf('</AppDialog>'))
expectWorkbenchDialogs(list)
expect(dialog).toContain('width="min(560px, calc(100vw - 28px))"')
expect(dialog).toContain(':confirm-loading="uploading"')
expect(dialog).toContain('class="density-table density-table--editable"')
expect(dialog).toContain('label-position="left"')
expect(dialog).toContain('class="density-dialog-note"')
expect(dialog).not.toContain('upload-scope-field')
expect(list).not.toContain('.upload-file-name strong')
for (const source of [list, detail]) expect(source).toContain("customClass: 'admin-density-confirm'")
})
it('uses shared frames, continuous tables and readonly facts for fleet quote dialogs', () => {
const editor = read('modules/overseas-master-data/components/FleetQuoteEditor.vue')
const importer = read('modules/overseas-master-data/components/FleetQuoteImport.vue')
for (const source of [editor, importer]) {
const [dialog] = expectWorkbenchDialogs(source)
expect(dialog!.props.some(prop => prop.type === 6 && prop.name === 'fullscreenable')).toBe(true)
expect(dialog!.props.some(prop => prop.type === 7 && prop.name === 'bind' && prop.arg?.type === 4 && prop.arg.content === 'confirm-loading' && prop.exp?.type === 4 && prop.exp.content === 'saving')).toBe(true)
expect(source).toContain('density-table--editable')
if (source === importer) expect(source).toContain('data-tone="work"')
expect(source).toContain('data-tone="source"')
expect(source).toContain('class="density-actions density-actions--footer"')
expect(source).not.toContain('label-position="top"')
const css = parse(source).descriptor.styles.map(style => style.content).join('')
expect(css).not.toMatch(/font-size|background|border|el-dialog__body|max-height/)
}
expect(editor).toContain('v-if="readonly"')
expect(editor).not.toContain('data-tone="work"')
expect(editor).toContain('class="density-form density-form--outlined density-dialog-sections"')
expect(read('modules/overseas-master-data/pages/FleetQuotesView.vue')).toContain('<RowActions text-actions')
expect(editor).toContain('class="density-fields fleet-quote-facts"')
const confirmations = confirmationClasses(read('modules/overseas-master-data/pages/FleetQuotesView.vue'))
expect(confirmations.length).toBeGreaterThan(0)
for (const classes of confirmations) expect(classes).toContain('admin-density-confirm')
})
it('keeps pricing dialogs on shared tables and sections without local form paint', () => {
const rate = read('modules/overseas-master-data/pages/ExchangeRatesView.vue')
expect(rate).toContain('class="density-form density-form--outlined"')
const quote = read('modules/overseas-master-data/pages/OverseasQuotesView.vue')
for (const source of [rate, quote]) {
const dialogs = expectWorkbenchDialogs(source)
expect(dialogs.length).toBeGreaterThan(0)
for (const dialog of dialogs) {
expect(dialog.loc.source).not.toContain('label-position="top"')
expect(dialog.loc.source).not.toContain('#header')
}
expect(source).toContain('density-table--editable')
expect(source).not.toContain('line-grid')
expect(source).not.toContain('form-grid')
}
expect(quote.match(/fullscreenable/g)).toHaveLength(2)
expect(quote).toContain('data-tone="summary"')
expect(quote).toContain('data-tone="source"')
expect(quote).not.toContain('data-tone="work"')
expect(quote).toContain('class="density-form density-form--outlined density-dialog-sections"')
const css = parse(quote).descriptor.styles.map(style => style.content).join('')
expect(css).not.toMatch(/font-size|background|border|el-dialog__body|max-height/)
})
it('keeps the supplier license dialog styled at its own teleported root', () => {
const source = read('modules/suppliers/components/SupplierLicenseDialog.vue')
expectWorkbenchDialogs(source)
expect(source).toContain('width="min(560px, calc(100vw - 24px))"')
expect(source).toContain('aria-label="营业执照信息"')
const sections = elementsOf(source, 'section')
expect(sections).toHaveLength(1)
expect(classesOf(sections[0]!)).toEqual(expect.arrayContaining(['density-dialog-section', 'workbench-evidence']))
expect(sections[0]!.props.some(prop => prop.type === 6 && prop.name === 'data-tone' && prop.value?.content === 'source')).toBe(true)
expect(source).toContain('class="density-table license-info-table"')
expect(source).not.toContain('license-card')
expect(source).not.toContain('box-shadow')
})
it('uses identical authored tab paint for native domestic and Element overseas tabs', () => {
const element = '.admin-density .density-element-tabs > .el-tabs__header .el-tabs__nav .el-tabs__item'
const rules: string[][] = []
cssRoot(shared).walkRules(rule => { rules.push(rule.selectors) })
for (const [nativeSuffix, elementSuffix] of [['', ''], ['::after', '::after'], [':hover', ':hover'], ['.active', '.is-active'], ['.active::after', '.is-active::after'], [':focus-visible', ':focus-visible']]) {
expect(rules.some(selectors => selectors.includes('.admin-density .density-tabs > button' + nativeSuffix) && selectors.includes(element + elementSuffix))).toBe(true)
}
const domestic = read('modules/unified-orders/pages/UnifiedOrdersView.vue')
const overseasShell = read('modules/overseas-warehouse/components/OverseasDetailShell.vue')
const truckDetail = read('modules/overseas-business/pages/TruckOrderDetailView.vue')
for (const className of ['density-detail-header', 'density-detail-heading', 'density-detail-title']) {
expect(domestic).toContain(className)
expect(overseasShell).toContain(className)
expect(truckDetail).toContain(className)
}
expect(truckDetail).toContain('density-detail-layout')
expect(truckDetail).toContain('class="truck-detail-grid"')
expect(domestic).toContain('<DetailPageLayout')
expect(domestic).toContain('order-info-grid density-fields')
expect(read('modules/overseas-warehouse/pages/LastMileOrderDetailView.vue')).toContain('density-detail-overview')
})
it('covers the approved overseas pages across the latest menu groups, details and both return inventory modes', () => {
const paths = getNavigationLeaves(adminNavigation).map(node => node.path)
expect(paths).toEqual(expect.arrayContaining(['last-mile-orders', 'inbound-orders', 'locations', 'dispatches', 'outbounds', 'delivery-orders', 'relabel-orders', 'pallets', 'inventory', 'return-inventory', 'return-cartons'].map(path => '/overseas-warehouse/' + path)))
const routes = read('modules/overseas-warehouse/routes.ts')
for (const mode of ['inventory', 'cartons']) expect(routes).toContain("mode: '" + mode + "'")
for (const file of overseasConsumers) {
const source = read('modules/' + file)
expect(source, file).toContain('@/styles/adminDensity.css')
expect(parse(source).errors, file).toEqual([])
if (file.endsWith('DetailView.vue') && !/OutboundDetail|DeliveryOrderDetail/.test(file)) {
const root = parse(source).descriptor.template!.ast!.children.find(node => node.type === 1) as ElementNode
expect(root.tag).toMatch(/^Overseas(?:DetailShell|ReadOnlyDetail)$/)
expect(root.props.some(prop => prop.type === 6 && prop.name === 'dense')).toBe(true)
expect(classesOf(root)).toContain('admin-density')
}
}
})
it('keeps contained overseas tab content and external cargo tab bodies separate', () => {
expect(shared).toContain('.density-element-tabs:not(.density-contained-tabs) > .el-tabs__content { display: none; }')
for (const file of ['LastMileOrderDetailView', 'DeliveryOrderDetailView', 'DispatchDetailView']) {
const source = read('modules/overseas-warehouse/pages/' + file + '.vue')
const ast = parse(source).descriptor.template!.ast!
let panes = 0
const hasPaneBody = (nodes: typeof ast.children): boolean => nodes.some(child => child.type === 1 && (
child.tag !== 'template' || !child.props.some(prop => prop.type === 7 && prop.name === 'slot' && prop.arg?.type === 4 && prop.arg.content === 'label') && hasPaneBody(child.children)
))
const visit = (nodes: typeof ast.children) => { for (const node of nodes) {
if (node.type !== 1) continue
if (node.tag === 'el-tabs') expect(classesOf(node)).toContain('density-contained-tabs')
if (node.tag === 'el-tab-pane') {
panes++
expect(hasPaneBody(node.children), file + ': pane retains body').toBe(true)
expect(node.loc.source).toContain('density-tab-icon')
}
visit(node.children)
} }
visit(ast.children)
expect(panes).toBe(file === 'LastMileOrderDetailView' ? 7 : 4)
}
})
it('fits the five dispatch vehicle fields in one desktop row with read-only detail tabs', () => {
const source = read('modules/overseas-warehouse/pages/DispatchDetailView.vue')
const css = parse(source).descriptor.styles.map(style => style.content).join('\n')
const declarations = (source: string, selector: string) => {
const values: Record<string, string> = {}
cssRoot(source).walkRules(rule => {
if (rule.selector === selector && rule.parent?.type !== 'atrule') rule.walkDecls(decl => { values[decl.prop] = decl.value })
})
return values
}
expect(declarations(css, '.dispatch-detail .vehicle-fields')['grid-template-columns']).toBe('minmax(0, 1.3fr) minmax(0, 1.6fr) minmax(0, 1.2fr) minmax(0, 1.5fr) minmax(0, 0.8fr)')
expect(declarations(css, '.dispatch-detail .vehicle-fields > div')['grid-template-columns']).toBe('max-content minmax(0, 1fr)')
expect(css).not.toContain('.dispatch-actions')
expect(declarations(shared, '.admin-density .density-contained-tabs > .el-tabs__content').padding).toBe('var(--admin-space-5)')
expect(declarations(shared, '.admin-density .density-contained-tabs.density-contained-tabs--flush > .el-tabs__content').padding).toBe('0')
expect(elementsOf(source, 'el-tabs').every(node => classesOf(node).includes('density-contained-tabs--flush'))).toBe(true)
})
it('opts shared overseas detail and relationship wrappers in only at approved callers', () => {
for (const file of ['modules/overseas-warehouse/components/OverseasDetailShell.vue', 'modules/overseas-warehouse/components/OverseasReadOnlyDetail.vue', 'shared/components/InboundCodeRelationshipDialog.vue']) {
const source = read(file)
expect(source).toContain('dense?: boolean')
const ast = parse(source).descriptor.template!.ast!
const root = ast.children.find(node => node.type === 1) as ElementNode
for (const workbench of [false, true]) {
expect(classesOf(root, false, true, 'DOMESTIC', workbench).some(name => name.startsWith('admin-density')), file).toBe(true)
expect(classesOf(root, false, false, 'DOMESTIC', workbench).some(name => name.startsWith('admin-density')), file).toBe(false)
if (file.endsWith('OverseasDetailShell.vue')) {
for (const dense of [false, true]) expect(classesOf(root, false, dense, 'DOMESTIC', workbench).includes('admin-workbench')).toBe(workbench)
}
}
}
expect(read('modules/overseas-warehouse/components/OverseasReadOnlyDetail.vue')).toContain(':dense="dense"')
expect(read('modules/overseas-warehouse/components/OverseasReadOnlyDetail.vue')).toContain(':workbench="workbench"')
expect(read('modules/overseas-warehouse/pages/InventoryTransactionsView.vue')).toContain('<InboundCodeRelationshipDialog dense')
expect(read('modules/inbounds/pages/InboundOrdersView.vue')).toContain('<InboundCodeRelationshipDialog dense')
for (const file of ['CartonDetailView', 'ProductDetailView']) expect(read('modules/overseas-warehouse/pages/' + file + '.vue')).not.toContain(' dense')
})
it('covers five cargo menus, independent detail routes and both workbench modes', () => {
const navigation = read('core/navigation/adminNavigation.ts')
for (const path of ['/ocean-bookings', '/trucking', '/container-loading/domestic', '/loading-lists', '/loading-plans']) expect(navigation).toContain(`path: '${path}'`)
for (const [prefix, workbench] of [['Domestic', 'DOMESTIC'], ['Overseas', 'OVERSEAS']]) {
for (const [suffix, component] of [['ContainersView', 'ContainersView'], ['ContainerDetailView', 'ContainerDetailView']]) {
expect(read(`modules/container-loading/pages/${prefix}${suffix}.vue`)).toContain(suffix === 'ContainersView' ? `<ContainerWorkbenchView stage="${workbench}"` : `<${component} workbench="${workbench}"`)
const ast = parse(read(`modules/container-loading/pages/${component}.vue`)).descriptor.template!.ast!
const root = ast.children.find(node => node.type === 1) as ElementNode
expect(classesOf(root, false, true, workbench)).toContain('admin-density')
}
}
const detail = read('modules/container-loading/pages/ContainerDetailView.vue')
for (const component of ['CarrierScheduleQueryPanel', 'LoadingListDetailDrawer', 'PalletLabelPrintDialog']) expect(detail).toContain(`<${component} dense`)
expect(read('modules/container-loading/pages/ContainersView.vue')).toContain('<ContainerSimulationCreate dense')
for (const file of cargoConsumers) expect(read(`modules/${file}`), `${file}: direct entry`).toContain('@/styles/adminDensity.css')
})
it('enables confirmed loading content and schedule overlays only for opted-in callers', () => {
for (const file of ['loading-lists/components/LoadingListDetailDrawer.vue', 'port-schedules/components/CarrierScheduleQueryPanel.vue']) {
const ast = parse(read(`modules/${file}`)).descriptor.template!.ast!
const root = ast.children.find(node => node.type === 1) as ElementNode
for (const embedded of [false, true]) {
expect(classesOf(root, false, true, 'DOMESTIC', false, embedded)).toContain('admin-density')
expect(classesOf(root, false, false, 'DOMESTIC', false, embedded)).not.toContain('admin-density')
if (file.includes('LoadingListDetail')) {
expect(classesOf(root, false, true, 'DOMESTIC', false, embedded).includes('workbench-panel')).toBe(!embedded)
expect(classesOf(root, false, true, 'DOMESTIC', false, embedded).includes('is-compact')).toBe(!embedded)
}
}
if (file.includes('CarrierSchedule')) {
const dialog = root.children.find(node => node.type === 1 && node.tag === 'AppDialog') as ElementNode
expect(classesOf(dialog, false, true)).toContain('admin-density-dialog')
expect(classesOf(dialog, false, false)).not.toContain('admin-density-dialog')
}
}
})
it('keeps cargo native tables, row actions and detail tabs connected to shared styles', () => {
for (const file of cargoConsumers) {
const ast = parse(read(`modules/${file}`)).descriptor.template!.ast!
const visit = (nodes: typeof ast.children, inCell = false, insideDensity = false, insideEditor = false) => {
for (const node of nodes) {
if (node.type !== 1) continue
const classes = classesOf(node)
const enabled = insideDensity || classes.some(name => ['admin-density', 'admin-density-dialog'].includes(name))
const editable = insideEditor || classes.includes('density-table--editable')
if (node.tag === 'table') {
expect(classes, `${file}: native table`).toContain('density-table')
expect(insideDensity, `${file}: native table ancestor`).toBe(true)
}
if (node.tag === 'el-tabs') expect(classes).toContain('density-element-tabs')
if (inCell && !editable && node.tag === 'el-button') {
expect(classes, `${file}: native row action`).toContain('density-action')
expect(node.children.some(child => child.type === 2 && child.content.trim() || child.type === 5), `${file}: empty action`).toBe(true)
}
visit(node.children, inCell || node.tag === 'td', enabled, editable)
}
}
visit(ast.children)
}
})
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 (classes.includes('density-confirm-dialog')) {
expect(classes, `${file}: confirmation density`).toContain('admin-density-dialog')
checked += 1
}
if (node.tag === 'DataTable' || node.tag === 'LoadingListDetailDrawer' || classes.some(name => ['density-action', 'density-actions', 'density-tabs', 'density-fields', 'density-table', 'density-tab-icon', 'density-back'].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 approved domestic pages across the latest menu groups and loads density CSS on direct entry', () => {
const paths = getNavigationLeaves(adminNavigation).map(node => node.path)
expect(paths).toEqual(expect.arrayContaining(domesticPages.map(page => page.path)))
for (const page of domesticPages) {
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('keeps customer dialogs on shared sections, editors and preview geometry', () => {
const customers = read('modules/customers/pages/CustomersView.vue')
const detail = read('modules/customers/pages/CustomerDetailView.vue')
const editor = customers.split('<AppDialog v-model="editorOpen"')[1]!.split('</AppDialog>')[0]!
const sections = [...editor.matchAll(/<section\b[^>]*>/g)].map(match => match[0])
expect(sections).toHaveLength(3)
for (const section of sections) {
expect(section).toContain('density-dialog-section')
expect(section).not.toContain('data-tone=')
}
expect(customers).toContain('density-table density-table--editable customer-editor-table')
expect(customers).toContain('density-actions density-actions--footer')
expect(customers).not.toContain('<FormSection')
expect(customers).not.toContain('<el-dialog')
for (const source of [customers, detail]) {
expect(source).toContain('customer-qualification-preview')
expect(source).toContain('data-tone="source"')
expect(source).not.toMatch(/\.qualification-preview \{[^}]*overflow:auto/)
}
})
it('shares dense editable sections between both customer fee entries and billing rules', () => {
for (const file of ['customers/CustomerOverseasStorageFeePanel.vue', 'customers/pages/OverseasStorageFeesView.vue', 'customers/components/CustomerBillingRulesPanel.vue']) {
const source = read(`modules/${file}`)
expect(source, file).toContain('density-table density-table--editable')
expect(source, file).toContain(file === 'customers/pages/OverseasStorageFeesView.vue' ? 'class="density-dialog-section"' : 'class="density-dialog-section" data-tone="work"')
expect(source, file).toContain('fullscreenable')
expect(source, file).not.toContain('class="dialog-actions"')
}
})
it('covers both customer menu entries and their shared detail primitives', () => {
const navigation = read('core/navigation/adminNavigation.ts')
const customers = navigation.slice(navigation.indexOf("key: 'customers'"), navigation.indexOf("key: 'domestic-warehouse'"))
expect([...customers.matchAll(/path: '/g)]).toHaveLength(customerPages.length)
for (const page of customerPages) {
expect(customers).toContain(`title: '${page.title}'`)
expect(customers).toContain(`path: '${page.path}'`)
}
expect(read('modules/customers/pages/CustomerDetailView.vue')).toContain('class="back-button density-back"')
})
it('uses shared actions across domestic tables and explicit classes on teleported dialogs', () => {
for (const file of [...new Set([...densityPages, ...consumers])]) {
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 (densityPages.includes(file) && !selfOperatedPages.includes(file) && 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')
if (['AppDrawer', 'el-drawer'].includes(node.tag)) expect(classesOf(node), `${file}: teleported drawer`).toContain('admin-density-drawer')
visit(node.children, isActionSlot, node.tag)
}
}
visit(template.children)
}
})
it('keeps the collection monitor in the Admin theme without legacy list controls', () => {
const page = read('modules/self-operated/pages/CollectionView.vue')
const monitor = read('modules/self-operated/components/CollectionAccountMonitor.vue')
expect(page).toContain('class="admin-density admin-workbench"')
expect(page).toContain('@/styles/adminDensity.css')
expect(page).toContain('@/styles/adminWorkbench.css')
expect(page).toContain('<CollectionAccountMonitor')
expect(page).not.toContain('<DataTable')
expect(monitor).toContain('workbench-panel')
expect(monitor).not.toMatch(/--el-[\w-]+\s*:/)
})
it('opts the confirmed self-operated pages into integrated text actions without changing older outlined consumers', () => {
expect(getNavigationLeaves(adminNavigation).map(node => node.path)).toEqual(expect.arrayContaining([
'/self-operated/sales', '/self-operated/inventory', '/self-operated/collection',
]))
for (const file of selfOperatedPages) {
const template = parse(read(`modules/${file}`)).descriptor.template!.ast!
let tables = 0
const visit = (nodes: typeof template.children) => {
for (const node of nodes) {
if (node.type !== 1) continue
if (node.tag === 'DataTable') {
expect(node.props.some(prop => prop.type === 6 && prop.name === 'appearance' && prop.value?.content === 'integrated'), `${file}: integrated text actions`).toBe(true)
tables += 1
}
visit(node.children)
}
}
visit(template.children)
expect(tables).toBeGreaterThan(0)
}
})
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('connects configurable surface colors and semantic hover states to their consumers', () => {
const root = cssRoot(shared)
const rules = new Map<string, Record<string, string>>()
root.walkRules(rule => {
const declarations: Record<string, string> = {}
rule.walkDecls(decl => { declarations[decl.prop] = decl.value })
rules.set(rule.selector, declarations)
})
const stripeSelector = '.admin-density .el-table__body tr.el-table__row--striped:not(:where(:hover, .hover-row, .current-row)) > td.el-table__cell:not(:where(.hover-cell))'
expect(rules.get(stripeSelector)?.background).toBe('var(--admin-row-stripe)')
const stripeFixture = document.createElement('div')
stripeFixture.innerHTML = '<div class="admin-density"><table class="el-table__body"><tbody><tr class="el-table__row--striped"><td class="el-table__cell"></td></tr></tbody></table></div>'
const row = stripeFixture.querySelector('tr')!
const cell = stripeFixture.querySelector('td')!
expect(cell.matches(stripeSelector)).toBe(true)
for (const state of ['hover-row', 'current-row']) {
row.classList.add(state)
expect(cell.matches(stripeSelector)).toBe(false)
row.classList.remove(state)
}
cell.classList.add('hover-cell')
expect(cell.matches(stripeSelector)).toBe(false)
cell.classList.remove('hover-cell')
cell.innerHTML = '<table class="el-table__body"><tbody><tr><td class="el-table__cell"></td></tr></tbody></table>'
expect(cell.querySelector('td')!.matches(stripeSelector)).toBe(false)
expect(rules.get('.admin-density-dialog .el-descriptions__label.el-descriptions__cell')?.background).toBe('var(--admin-label-bg)')
const horizontalLabelSelector = '.admin-density-dialog .el-form-item:not(.el-form-item--label-top) .el-form-item__label'
expect(rules.get(horizontalLabelSelector)?.['align-items']).toBe('center')
const labelFixture = document.createElement('div')
labelFixture.innerHTML = '<div class="admin-density-dialog"><form class="el-form"><div class="el-form-item el-form-item--label-left"><label class="el-form-item__label">计划送仓日期</label></div></form></div>'
const formItem = labelFixture.querySelector('.el-form-item')!
const label = labelFixture.querySelector('label')!
expect(label.matches(horizontalLabelSelector)).toBe(true)
formItem.className = 'el-form-item el-form-item--label-right'
expect(label.matches(horizontalLabelSelector)).toBe(true)
formItem.className = 'el-form-item el-form-item--label-top'
expect(label.matches(horizontalLabelSelector)).toBe(false)
expect(rules.get('.admin-density-dialog.el-dialog.admin-density-workspace > .el-dialog__body')?.flex).toBe('1 1 auto')
for (const tone of ['warning', 'purple', 'primary', 'success', 'danger']) {
expect(rules.get(`:is(.admin-density, .admin-density-dialog) .density-action[data-tone="${tone}"]`)?.['--action-hover-bg']).toBe(`var(--admin-action-${tone}-hover)`)
}
expect(rules.get(':is(.admin-density, .admin-density-dialog) .el-button.density-action:hover:not(:disabled)')?.background).toBe('var(--action-hover-bg)')
})
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.trim(), file + ': empty selector').not.toBe('')
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$/)
// Decorative artwork is not a form control or data row.
const decorativeBrand = file === 'ocean-bookings/pages/OceanBookingDetailView.vue' && rule.selector === '.booking-brand'
if (desktop && !decorativeBrand && /^(height|min-height)$/.test(decl.prop)) expect(decl.value, `${file}: ${rule.selector}`).not.toMatch(/^(28|32|36|38|64)px$/)
})
})
})
it('keeps measurement sections within the clipped panel and gives long references room', () => {
const dialog = parse(read('modules/inbounds/measurement/InboundMeasurementDialog.vue')).descriptor
const panelCss = parse(read('modules/inbounds/measurement/InboundMeasurementPanel.vue')).descriptor.styles.map(style => style.content).join('\n')
const differenceStyles = new Map<string, Record<string, string>>()
cssRoot(panelCss).walkRules(rule => {
const declarations: Record<string, string> = {}
rule.walkDecls(decl => { declarations[decl.prop] = decl.value })
differenceStyles.set(rule.selector, declarations)
})
expect(differenceStyles.get('.measurement-comparison__difference')?.color).toBe('var(--el-color-danger-dark-2)')
expect(differenceStyles.get('.measurement-deviation-alert')).toMatchObject({
'--el-color-warning': 'var(--el-color-danger-dark-2)',
'--el-color-warning-dark-2': 'var(--el-color-danger-dark-2)',
})
const layout = read('modules/inbounds/measurement/measurementDensity.css')
const summaryMargins: string[] = []
for (const css of [layout, ...dialog.styles.map(style => style.content)]) {
cssRoot(css).walkRules(rule => {
if (!/\.measurement-(summary-section|declaration|items-section)(?![\w-])/.test(rule.selector)) return
rule.walkDecls(decl => {
if (decl.prop.startsWith('margin')) {
expect(decl.value, `section must not escape the clipped panel: ${rule.selector}`).not.toMatch(/(^|\s)-\d/)
if (rule.selector.endsWith('.measurement-summary-section')) summaryMargins.push(decl.value)
}
})
})
}
expect(summaryMargins).toEqual(['0'])
const sectionStyles = new Map<string, Record<string, string>>()
cssRoot(layout).walkRules(rule => {
const declarations: Record<string, string> = {}
rule.walkDecls(decl => { declarations[decl.prop] = decl.value })
sectionStyles.set(rule.selector, declarations)
})
for (const [section, background] of [
['summary-section', 'var(--admin-surface, #fff)'], ['declaration', 'var(--admin-dialog-source-bg)'], ['items-section', 'var(--admin-surface, #fff)'],
]) {
expect(sectionStyles.get(`.inbound-measurement-workspace-dialog.inbound-density .measurement-${section}`)).toMatchObject({
padding: 'var(--admin-space-4, 10px) var(--admin-space-5, 12px)',
background,
})
}
expect(sectionStyles.get('.inbound-measurement-workspace-dialog.inbound-density .measurement-deviation-alert')?.background).toBe('var(--admin-surface, #fff)')
// Shared density must not reset the sections' horizontal gutters.
expect(sectionStyles.get('.inbound-density.admin-density-dialog .measurement-section')).toEqual({
'padding-block': 'var(--admin-space-4, 10px)',
})
expect(layout).toContain('.measurement-document-number { display: flex; flex: 1;')
expect(layout).toContain('.measurement-platform-fields { display: flex; flex: 1 1 520px;')
expect(layout).toContain('.measurement-platform-fields > .measurement-expected-pallets { flex: 0 0 180px; }')
for (const style of dialog.styles) {
expect(style.content).not.toMatch(/\.measurement-(summary-section|metrics|declaration|items-section)\b/)
}
})
it('keeps desktop table toolbar dimensions owned by DataTable without cascade overrides', () => {
const tableCss = parse(read('shared/components/DataTable.vue')).descriptor.styles.map(style => style.content).join('\n')
expect(tableCss).toContain('.data-table > .data-table__toolbar.has-inline-filters')
expect(tableCss).toContain('.data-table__toolbar.has-inline-filters > .data-table__filters { flex: 1 1 0; min-width: 0; }')
const dimensions: Record<string, string[]> = { padding: [], 'min-height': [] }
cssRoot(tableCss).walkRules(rule => {
if (!rule.selectors.includes('.data-table__toolbar') && !rule.selectors.includes('.data-table__toolbar.is-compact')) return
// Preserve the existing mobile layout; the shared desktop dimensions have one owner.
if (rule.parent?.type === 'atrule' && /max-width/.test(rule.parent.params)) return
rule.walkDecls(decl => { if (dimensions[decl.prop]) dimensions[decl.prop].push(decl.value) })
})
expect(dimensions).toEqual({ padding: ['var(--admin-space-2, 6px) var(--admin-space-5, 12px)'], 'min-height': ['44px'] })
for (const { file, css } of [{ file: 'adminDensity.css', css: shared }, ...styles]) cssRoot(css).walkRules(rule => {
if (!/\.data-table__toolbar(?:\b|\.)/.test(rule.selector)) return
rule.walkDecls(decl => {
expect(decl.prop, `${file}: toolbar dimensions belong in DataTable`).not.toMatch(/^(padding(?:-.+)?|height|min-height)$/)
})
})
})
it('gives simulation creation an opt-in comfortable editor frame', () => {
const source = read('modules/container-loading/components/ContainerSimulationCreate.vue')
expect(source).toContain("'admin-density-dialog--comfortable': dense")
expect(source).toContain('width="min(800px, calc(100vw - 32px))"')
expect(shared).toContain('.admin-density-dialog.admin-density-dialog--comfortable.el-dialog { --el-component-size: 40px; border-radius: 12px; }')
expect(shared).toContain('> .el-form-item__content > .el-select > .el-select__wrapper { min-height: 40px; }')
expect(shared).not.toContain('.admin-density-dialog.admin-density-dialog--comfortable.el-dialog .el-form-item')
expect(shared).not.toContain('.admin-density-dialog.admin-density-dialog--comfortable.el-dialog .el-select__wrapper')
})
it('keeps conditional shared consumers opt-in instead of enabling other menus', () => {
const actionWeights = new Map<string, string>()
cssRoot(read('styles/adminTextActions.css')).walkRules(rule => {
rule.walkDecls('font-weight', declaration => { actionWeights.set(rule.selector, declaration.value) })
})
expect(actionWeights.get('.row-actions.admin-text-actions .el-button.el-button')).toBe('400')
expect(actionWeights.get('.admin-text-actions.el-popper .el-dropdown-menu__item')).toBe('400')
expect(read('modules/overseas-master-data/pages/OwnedBusinessView.vue')).toContain('class="row-actions density-actions admin-text-actions"')
const dispatches = read('modules/overseas-warehouse/pages/DispatchesView.vue')
expect(dispatches).toContain('appearance="integrated"')
expect(dispatches).toContain('<template #filters><div class="integrated-query-group">')
expect(dispatches).toContain('class="density-status-tabs" role="tablist"')
expect(dispatches).not.toContain('class="surface-panel table-panel"')
const calendar = read('modules/sailing-calendar/pages/SailingCalendarView.vue')
expect(calendar).toContain('appearance="integrated" :data="[]" :columns="[]" column-settings-mode="hide"')
expect(calendar).toContain('<template #results>')
expect(calendar).not.toContain('class="calendar-toolbar surface-panel"')
expect(read('shared/components/DataTable.vue')).toContain('margin-left: var(--cc-list-action-sibling-margin, 8px)')
expect(shared).toContain('.density-actions { --cc-list-action-sibling-margin: 0px;')
expect(read('modules/loading-lists/components/LoadingListsView.vue')).toContain('class="business-page loading-lists-page admin-density"')
const palletPrintDialog = read('modules/loading-lists/components/PalletLabelPrintDialog.vue')
expect(palletPrintDialog).toContain("'admin-density-dialog': dense")
expect(palletPrintDialog).not.toContain(':data-tone=')
expect(read('modules/container-loading/components/ContainerSimulationCreate.vue')).toContain("'admin-density-dialog': dense")
expect(read('modules/container-loading/components/ContainerSimulationCreate.vue')).toContain('width="min(800px, calc(100vw - 32px))"')
for (const file of ['loading-lists/components/LoadingListDetailDrawer.vue', 'loading-lists/components/PalletLabelPrintDialog.vue', 'container-loading/components/ContainerSimulationCreate.vue']) {
const template = parse(read(`modules/${file}`)).descriptor.template!.ast!
const visit = (nodes: typeof template.children) => {
for (const node of nodes) {
if (node.type !== 1) continue
expect(classesOf(node, false).filter(name => /^(admin-density|density-)/.test(name)), `${file}: unopted consumer`).toEqual([])
visit(node.children)
}
}
visit(template.children)
}
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')
}
})
it('halves text action spacing through shared rules while retaining outlined action spacing', () => {
const declarations = new Map<string, Record<string, string>>()
cssRoot(read('styles/adminTextActions.css')).walkRules(rule => {
const values: Record<string, string> = {}
rule.walkDecls(decl => { values[decl.prop] = decl.value })
declarations.set(rule.selector, values)
})
expect(declarations.get('.row-actions.admin-text-actions')).toMatchObject({
'--cc-row-action-gap': 'calc(var(--admin-space-2, 6px) / 2)',
'--cc-list-action-sibling-margin': '0px',
gap: 'var(--cc-row-action-gap)',
})
const button = declarations.get('.row-actions.admin-text-actions .el-button.el-button')!
expect(button['padding-inline']).toBe('calc(var(--admin-space-4, 10px) / 2)')
expect(button).not.toHaveProperty('height')
expect(button).not.toHaveProperty('font-size')
const groups = new Map<string, string>()
cssRoot(shared).walkRules(rule => {
if (rule.selector.endsWith(' .density-actions')) {
rule.walkDecls('gap', decl => { groups.set(rule.selector, decl.value) })
}
})
expect([...groups.values()]).toEqual([
'var(--cc-row-action-gap, var(--admin-space-2))',
'var(--cc-row-action-gap, var(--admin-space-2))',
])
})
it('keeps active last-mile order dialogs on shared sections without internal vertical caps', () => {
const source = read('modules/overseas-warehouse/pages/LastMileOrdersView.vue')
for (const model of ['transferOpen', 'containerConvertOpen', 'inboundPlanOpen', 'holdOpen', 'manualNodesOpen', 'reworkOpen']) {
const frame = source.match(new RegExp('<AppDialog[^>]*v-model="' + model + '"[\\s\\S]*?(?=\\n <|\\n <|$)'))?.[0] || ''
expect(frame, model).toContain('fullscreenable')
}
expect(source).toContain('data-tone="summary"')
expect(source).toContain('data-tone="source"')
expect(source).toContain('data-tone="work"')
expect(source).toContain('class="frozen-pallet-count"')
expect(source).not.toContain('max-height: 48vh')
expect(source).not.toContain('.manual-node-table { max-height: 420px')
expect(source).not.toContain('class="container-preview__metric')
const detail = read('modules/overseas-warehouse/pages/LastMileOrderDetailView.vue')
expect(detail).toContain('density-table density-table--editable fee-editor-table')
expect(detail).toContain('<section class="density-dialog-section" aria-label="尾程费用登记">')
expect(detail).toContain('label-position="left" label-width="110px" :disabled="saving"')
expect(detail).not.toContain('.fee-form-grid')
expect(read('modules/overseas-warehouse/components/WarehouseTransferReviewDialog.vue')).toContain('data-tone="source"')
})
it('keeps dispatch planning on shared sections and cargo selection in the three-column board', () => {
const source = read('modules/overseas-warehouse/components/SmartDispatchDialog.vue')
const board = read('modules/overseas-warehouse/components/SmartDispatchCargoBoard.vue')
const fields = read('modules/overseas-warehouse/components/DispatchResourceFields.vue')
expect(source).toContain('fullscreenable')
expect(source).toContain('aria-label="排车设置"')
expect(source).not.toContain('<DispatchResourceFields')
expect(source).toContain('<SmartDispatchCargoBoard')
expect(source).toContain('label-position="left" label-width="70px"')
expect(source).toContain('class="dispatch-context-toolbar"')
expect(source).not.toContain('class="density-dialog-section dispatch-draft-toolbar"')
expect(source).toContain('<template #vehicle-controls>')
expect(source).toContain('class="density-form dispatch-setup-fields"')
expect(source).toContain('<el-form-item v-if="mode === \'empty\'" label="起运仓">')
expect(source).toContain('<template #order-controls>')
expect(source).not.toContain('class="dispatch-warehouse-field" label="起运仓"')
expect(source).not.toContain('data-tone="work"')
expect(source).not.toContain('data-tone="summary"')
expect(source).not.toContain('data-tone="source"')
expect(board).toContain('class="dispatch-cargo-board"')
expect(board).toContain('class="density-table cargo-board-table"')
const boardCss = parse(board).descriptor.styles.map(style => style.content).join('\n')
const orderViewport = boardCss.match(/\.cargo-board-table-wrap\s*\{([^}]+)\}/)?.[1] || ''
const palletViewport = boardCss.match(/\.cargo-board-pallet-list\s*\{([^}]+)\}/)?.[1] || ''
for (const viewport of [orderViewport, palletViewport]) {
expect(viewport).toContain('max-height:')
expect(viewport).not.toMatch(/(?:^|;)\s*height:/)
}
expect(boardCss).not.toContain('min-height: 74px')
expect(boardCss).toContain('grid-template-rows: subgrid')
expect(boardCss).toContain('grid-row: 1 / span 2')
expect(boardCss).not.toMatch(/\.cargo-board-pallets\s*\{[^}]*margin-top:/)
expect(board).toContain('<slot name="vehicle-heading">')
expect(source).not.toContain('admin-density-workspace')
expect(board).toContain('aria-label="待排订单"')
expect(board).toContain('aria-label="托盘明细"')
expect(board).toContain('aria-label="车辆装载"')
expect(board).toContain('class="cargo-board-group-details"')
expect(source).not.toContain('height="420"')
expect(source).not.toContain('class="create-grid"')
expect(fields).toContain('continuous?: boolean')
expect(fields).toContain('v-if="continuous"')
expect(fields).toContain('density-table density-table--editable dispatch-resource-table')
expect(fields).toContain('class="resource-grid"')
})
it('keeps dispatch workflow and owned evidence on shared compact sections', () => {
const workflow = read('modules/overseas-warehouse/components/DispatchWorkflowDialog.vue')
const maintenance = read('modules/overseas-warehouse/components/DispatchVehicleMaintenanceForm.vue')
const evidence = read('modules/overseas-warehouse/components/DispatchEvidenceFiles.vue')
expect(workflow).toContain(':fullscreenable="width !== \'560px\'"')
expect(workflow).toContain('<DispatchVehicleMaintenanceForm')
expect(maintenance).toMatch(/<DispatchResourceFields\b[^>]*\bcontinuous\b/)
expect(maintenance).toContain('density-table density-table--editable maintenance-table')
expect(workflow).toContain('label-position="left" label-width="130px"')
expect(workflow).toContain(':confirm-loading="submitting || busy || uploadingEvidence"')
expect(workflow).not.toContain('aria-label="候选货物分页"')
expect(workflow).toContain("isReview.value ? '1100px'")
expect(workflow).toContain('aria-label="排车与预约"')
expect(workflow).toContain('aria-label="审核照片"')
for (const tone of ['source', 'work']) {
expect(workflow).toContain(`data-tone="${tone}"`)
}
expect(workflow).not.toMatch(/max-height="(?:380|420)"/)
expect(evidence).toContain('class="density-table dispatch-evidence-table"')
expect(evidence).toContain('fullscreenable :loading="previewBusy"')
expect(evidence).toContain('class="evidence-preview" :aria-busy="previewBusy"')
expect(evidence).not.toContain('aria-label="本车附件预览信息"')
expect(evidence).not.toContain('正在读取本车附件…')
expect(evidence).toContain('<LoadErrorState')
expect(evidence).toContain('<LineClampText')
for (const source of [workflow, evidence]) {
const styles = parse(source).descriptor.styles.map(style => style.content).join('\n')
expect(styles).not.toMatch(/border-left:|font-size:/)
cssRoot(styles).walkRules(rule => rule.walkDecls(decl => {
if (decl.prop !== 'background') return
expect(source).toBe(evidence)
expect(rule.selector).toMatch(/^\.evidence-photo-(?:preview|empty)$/)
expect(decl.value).toBe('var(--el-fill-color-lighter)')
}))
}
})
it('keeps outbound creation modes compact with one body scroll and complete candidate controls', () => {
const source = read('modules/overseas-warehouse/pages/OutboundsView.vue')
expect(source).toContain('width="min(1760px, calc(100vw - 32px))" fullscreenable')
expect(source).toContain('label-position="left" label-width="120px" :disabled="createLocked"')
expect(source).toContain('density-table density-table--editable outbound-editor-table')
expect(source).toContain('data-tone="source"')
expect(source).toContain('class="density-dialog-section" aria-label="出库模式"')
expect(source).toContain('class="density-dialog-section" aria-label="本次出库备注"')
expect(source).toContain(':confirm-loading="saving || inventoryLoading"')
expect(source).toContain('aria-label="全选本页订单"')
expect(source).toContain('class="return-list-card density-dialog-section"')
expect(source).toContain(':page-sizes="[5, 10, 20, 50]"')
expect(source).toContain('<ApiErrorMeta :details="candidateError"')
expect(source).not.toMatch(/max-height="(?:360|380|400)"/)
const styles = parse(source).descriptor.styles.map(style => style.content).join('\n')
expect(styles).not.toMatch(/background:|border-left:|font-size:|max-height:|overflow: auto/)
})
it('keeps delivery operations and documents on compact sections without read-only inputs', () => {
const source = read('modules/overseas-warehouse/pages/DeliveryOrdersView.vue')
const documents = read('modules/overseas-warehouse/components/DeliveryDocumentsDialog.vue')
expect(source).toContain('density-table density-table--editable delivery-return-sources')
expect(source).toContain('label-position="left" label-width="100px"')
expect(source).toContain('label-position="left" label-width="110px"')
expect(source).toContain(':confirm-loading="returnSaving || returnLoading"')
expect(source).toContain('width="min(720px, calc(100vw - 28px))" fullscreenable')
expect(documents).toContain('width="min(1040px, calc(100vw - 40px))" fullscreenable')
expect(documents).toContain('class="document-group density-dialog-section"')
expect(documents).toContain('class="density-actions row-actions admin-text-actions"')
expect(documents).not.toMatch(/data-tone="(?:summary|source|work)"/)
expect(documents).not.toContain('派送资料摘要')
expect(documents).toContain('<LineClampText :content="file.fileName"')
expect(documents).not.toContain(':model-value="deliveryNo" readonly')
expect(documents).not.toContain('max-height=')
const styles = parse(documents).descriptor.styles.map(style => style.content).join('\n')
expect(styles).not.toMatch(/background:|border-left:|font-size:|overflow: auto/)
})
it('shares relationship sections and fullscreen only through the explicit dense profile', () => {
const content = read('shared/components/InboundCodeRelationshipDialog.vue')
expect(content).toContain(':fullscreenable="dense"')
expect(content).toContain("{ 'density-dialog-sections': dense }")
expect(content.match(/'density-dialog-section': dense/g)).toHaveLength(3)
expect(content.match(/'density-dialog-section__title': dense/g)).toHaveLength(3)
expect(content).toContain(':data-tone="dense ? \'source\' : undefined"')
expect(content).toContain(':data-tone="dense ? \'summary\' : undefined"')
expect(content).not.toContain('.inbound-relationship-dialog.admin-density-dialog .relationship-section')
const styles = parse(content).descriptor.styles.map(style => style.content).join('\n')
cssRoot(styles).walkRules(rule => {
rule.walkDecls(decl => {
if (!['padding', 'background', 'border-radius', 'font-size', 'max-height'].includes(decl.prop)) return
expect(rule.selector, 'legacy visual declarations must stay outside dense').toMatch(/:not\(\.admin-density-dialog\)|:not\(\.density-dialog-section\)|:not\(\.density-fields\)/)
})
})
})
it('keeps return packing and carton dialogs on shared sections and one body scroll', () => {
const content = read('modules/overseas-warehouse/pages/ReturnInventoryManagementView.vue')
const dialogs = content.slice(content.indexOf('<AppDialog'))
expect(dialogs.match(/fullscreenable/g)).toHaveLength(3)
expect(dialogs).toContain('1380px')
expect(dialogs).toContain('1400px')
expect(dialogs).toContain('1080px')
expect(dialogs).toContain('label-position="left" label-width="110px"')
expect(dialogs).toContain('density-table density-table--editable')
expect(dialogs).toContain('data-tone="source"')
expect(dialogs).toContain('data-tone="summary"')
expect(dialogs).toContain('data-tone="work"')
expect(dialogs).not.toContain('<el-descriptions')
expect(dialogs).not.toContain('label-position="top"')
const styles = parse(content).descriptor.styles.map(style => style.content).join('\n')
expect(styles).not.toMatch(/max-height:|resize:|overflow: auto|border-radius:|font-size:|background:/)
expect(content).not.toContain('.packing-form :deep(.el-input__inner)')
})
it('keeps discard readonly sections and confirmation compact while retaining photo geometry', () => {
const content = read('modules/overseas-warehouse/pages/CargoDiscardsView.vue')
const dialog = content.slice(content.indexOf('<AppDialog'))
expectWorkbenchDialogs(content)
expect(dialog).toContain('fullscreenable')
expect(dialog).toContain('920px')
expect(dialog).toContain('data-tone="summary"')
expect(dialog).toContain('data-tone="source"')
expect(dialog).toContain('data-tone="work"')
expect(dialog).toContain('preview-teleported')
expect(dialog).toContain('<LineClampText')
expect(dialog).not.toContain('<el-timeline')
const confirmations = confirmationClasses(content)
expect(confirmations).toHaveLength(1)
expect(confirmations[0]).toEqual(expect.arrayContaining(['admin-density-confirm', 'admin-workbench', 'is-dialog']))
expect(content).toContain('height: 150px')
const styles = parse(content).descriptor.styles.map(style => style.content).join('\n')
expect(styles).not.toMatch(/background:|border-left:|font-size:|border-radius:/)
})
it('keeps overseas location editing and native confirmations on the shared dialog contract', () => {
const content = read('modules/overseas-warehouse/pages/LocationsView.vue')
const dialog = content.slice(content.indexOf('<AppDialog'))
expectWorkbenchDialogs(content)
expect(dialog).toContain('fullscreenable')
expect(dialog).toContain('density-table density-table--editable')
expect(dialog).toContain('label-position="left" label-width="110px" :disabled="formLocked"')
expect(dialog).toContain('data-tone="work"')
expect(dialog).not.toContain('label-position="top"')
expect(content).not.toContain('editor-grid')
const confirmations = confirmationClasses(content)
expect(confirmations).toHaveLength(2)
for (const classes of confirmations) expect(classes).toEqual(expect.arrayContaining(['admin-density-confirm', 'admin-workbench', 'is-dialog']))
expect(parse(content).descriptor.styles).toHaveLength(0)
})
it('keeps overseas pallet editing on one shared continuous table with native fullscreen', () => {
const content = read('modules/overseas-warehouse/pages/PalletsView.vue')
const dialog = content.slice(content.indexOf('<AppDialog'))
expect(dialog).toContain('fullscreenable')
expect(dialog).toContain('label-position="left" label-width="120px" :disabled="formLocked"')
expect(dialog).toContain('density-table density-table--editable pallet-editor-table')
expect(dialog).toContain('data-tone="work"')
expect(dialog).not.toContain('label-position="top"')
expect(dialog).not.toContain('<el-row')
expect(content).not.toContain('.el-form :deep(')
const styles = parse(content).descriptor.styles.map(style => style.content).join('\n')
const editorRules = new Map<string, Record<string, string>>()
cssRoot(styles).walkRules(rule => {
if (!rule.selector.includes('pallet-editor-table')) return
const values: Record<string, string> = {}
rule.walkDecls(decl => { values[decl.prop] = decl.value })
editorRules.set(rule.selector, values)
})
expect([...editorRules]).toEqual([
['.pallet-editor-table', { 'table-layout': 'fixed' }],
['.pallet-editor-table td:not([colspan])', { width: '50%' }],
])
})
it('keeps relabel creation, review and PDF previews compact without replacing document sizing', () => {
const list = read('modules/overseas-warehouse/pages/RelabelOrdersView.vue')
const detail = read('modules/overseas-warehouse/pages/RelabelOrderDetailView.vue')
expect(list).toContain('width="min(760px, calc(100vw - 28px))" fullscreenable')
expect(list).toContain('density-table density-table--editable relabel-editor-table')
expect(list).toContain('aria-label="换标审核原始资料"')
for (const tone of ['summary', 'source', 'work']) expect(list).toContain(`data-tone="${tone}"`)
expect(list).not.toContain('label-position="top"')
expect(list).not.toContain('packing-plan-dialog')
expect(detail).toContain('width="min(900px, calc(100vw - 28px))" fullscreenable')
expect(detail).toContain(':loading="previewLoading"')
expect(detail).toContain('height:min(72vh,760px)')
expect(detail).toContain('<th class="relabel-pdf-actions">操作(明细PDF)</th>')
expect(detail).not.toContain('item.destinationFbaWarehouseCode || item.destinationWarehouseId')
expect(parse(list).descriptor.styles.map(style => style.content).join('\n')).not.toMatch(/background:|border-left:|font-size:|max-height:/)
})
it('keeps inbound appointment, plan and move editors on opt-in continuous forms', () => {
const list = read('modules/overseas-warehouse/pages/InboundOrdersView.vue')
const detail = read('modules/overseas-warehouse/pages/InboundOrderDetailView.vue')
const fields = read('modules/overseas-warehouse/components/FbaAppointmentFields.vue')
expect(list).toContain('<FbaAppointmentFields continuous')
expect(list.match(/fullscreenable/g)).toHaveLength(2)
expect(list).toContain('class="density-dialog-section" aria-label="预约范围核对"')
expect(list).toContain('data-tone="source"')
expect(list).toContain('class="density-dialog-section" aria-label="目的仓登记"')
expect(list).not.toContain('v-model="planEditForm.version"')
expect(fields).toContain('continuous?: boolean')
expect(fields).toContain('v-if="continuous"')
expect(fields).toContain('density-table density-table--editable appointment-editor-table')
expect(fields).toContain('label-position="left" label-width="140px"')
expect(fields).toContain('label-position="top" class="appointment-fields density-form"')
const move = detail.slice(detail.indexOf('<AppDialog'))
expect(move).toContain('fullscreenable')
expect(move).toContain('aria-label="移库来源"')
expect(move).toContain('label-position="left" label-width="100px" :disabled="moveSaving || moveLocationsLoading"')
expect(move).not.toContain(':model-value="moveRow?.palletNo"')
})
it('keeps transfer allocation and receipt popups on shared sections and the native submit footer', () => {
const allocation = read('modules/overseas-warehouse/components/WarehouseTransferFulfillmentPanel.vue')
const receipt = read('modules/overseas-warehouse/components/WarehouseTransferReceiptPanel.vue')
for (const source of [allocation, receipt]) {
expect(source).toContain("import '@/styles/adminDensity.css'")
expect(source).toContain('class="density-dialog-sections"')
expect(source).toContain('data-tone="summary"')
expect(source).toContain('data-tone="source"')
expect(source).not.toMatch(/max-height:|overflow-y:/)
}
expect(allocation.match(/fullscreenable/g)).toHaveLength(1)
expect(receipt.match(/fullscreenable/g)).toHaveLength(3)
expect(allocation).toContain('row-key="inventoryPositionId" max-height=""')
expect(receipt).toContain('row-key="cartonMark" max-height=""')
expect(receipt).toContain('row-key="sku" max-height=""')
const forecast = receipt.split('v-model="increaseOpen"')[1]!.split('</AppDialog>')[0]!
expect(forecast).toContain(':confirm-loading="increasing"')
expect(forecast).toContain('@confirm="saveIncreaseForecast"')
expect(forecast).toContain('label-position="left" label-width="100px" :disabled="increasing"')
expect(forecast).toContain('density-table density-table--editable')
expect(forecast).not.toContain(':show-footer="false"')
expect(forecast).not.toContain('<el-button')
})
it('keeps settlement generation and cashiers on continuous forms with native fullscreen', () => {
const source = read('modules/finance/pages/FinanceFormalSettlementPage.vue')
const popups = source.slice(source.indexOf('<AppDialog'))
expect(popups.match(/fullscreenable/g)).toHaveLength(3)
expect(popups.match(/density-table density-table--editable/g)).toHaveLength(3)
expect(popups).toContain('label-position="left" label-width="120px" :disabled="createBusy"')
for (const flag of ['paymentBusy', 'receiptBusy']) expect(popups).toContain(`label-position="left" label-width="140px" :disabled="${flag}"`)
expect(popups).toContain('data-tone="summary"')
expect(popups).toContain('data-tone="work"')
expect(popups).toContain(':key="paymentUploadKey"')
expect(popups).toContain(':key="receiptUploadKey"')
expect(popups).not.toContain('label-position="top"')
expect(source).not.toContain('.remaining-preview')
expect(source).not.toContain('.settlement-operation-form { display: grid')
})
it('keeps fee and statement popup editors on the shared density and main body scroll', () => {
const arap = read('modules/finance/pages/FinanceCustomerArapView.vue').split('<AppDialog')[1]!
expect(arap).toContain('fullscreenable')
expect(arap).toContain('data-tone="source"')
expect(arap).toContain('data-tone="work"')
expect(arap).toContain('density-table density-table--editable')
expect(arap).toContain('label-position="left" label-width="110px" :disabled="reviewSaving"')
const statements = read('modules/finance/pages/FinanceCustomerStatementsView.vue')
const popups = statements.slice(statements.indexOf('<AppDialog'))
const createPopup = popups.split('</AppDialog>')[0]!
expect(createPopup).toContain('class="density-dialog-section workbench-evidence" aria-label="对账单生成设置"')
expect(createPopup).not.toMatch(/data-tone="(?:summary|source|work)"/)
expect(popups.match(/fullscreenable/g)).toHaveLength(2)
expect(popups).toContain('label-position="left" label-width="90px" :disabled="createBusy"')
expect(popups).toContain('label-width="110px" :disabled="adjustBusy"')
expect(popups).toContain('<OperationErrorToast :details="adjustError"')
expect(popups).toContain('data-tone="summary"')
expect(popups).not.toContain('max-height="420"')
expect(popups).not.toContain('label-position="top"')
expect(statements).not.toContain('.create-period-row')
})
it('uses the shared compact metric cards for finance statistics and settlement totals', () => {
expect(read('modules/finance/pages/FinanceReceivableStatisticsView.vue')).toContain('spacing="compact"')
expect(read('modules/finance/pages/FinanceFormalSettlementPage.vue')).toContain(':spacing="detailMode ? \'normal\' : \'compact\'"')
const financeStyles = read('modules/finance/financeDensity.css')
expect(financeStyles).toContain('.admin-workbench.receivable-statistics-page.admin-page.is-spacing-compact,')
expect(financeStyles).toContain('.admin-workbench.finance-settlement-page.admin-page.is-spacing-compact { gap: 0; }')
expect(financeStyles).not.toContain('.data-table__toolbar')
const baseStyle = parse(read('shared/components/AdminPage.vue')).descriptor.styles[0]!
const compiled = compileStyle({ source: baseStyle.content, filename: 'AdminPage.vue', id: 'data-v-spacing-check', scoped: true })
expect(compiled.errors).toHaveLength(0)
const baseSelector = compiled.code.match(/([^{}]+)\{\s*gap: 12px;/)![1]!.trim()
const specificity = (selector: string) => (selector.match(/\.[\w-]+|\[[^\]]+\]/g) ?? []).length
for (const page of ['receivable-statistics-page', 'finance-settlement-page']) {
const selector = `.admin-workbench.${page}.admin-page.is-spacing-compact`
expect(specificity(selector), 'must override scoped AdminPage even when it loads later').toBeGreaterThan(specificity(baseSelector))
}
for (const file of ['FinanceReceivableStatisticsView', 'FinanceFormalSettlementPage']) {
const content = read(`modules/finance/pages/${file}.vue`)
const summary = content.match(/<section class="finance-metric-grid"[^>]*v-loading="loading"[\s\S]*?<\/section>/)![0]
expect(content.indexOf(summary), file).toBeLessThan(content.indexOf('<template #filters>'))
const template = parse(content).descriptor.template!.ast!
function visit(node: { tag?: string; loc?: { source: string }; children?: unknown[] }, insideTable = false) {
if (node.tag === 'section' && node.loc?.source === summary) expect(insideTable, file).toBe(false)
for (const child of node.children ?? []) visit(child as typeof node, insideTable || node.tag === 'DataTable')
}
visit(template)
expect(summary.match(/workbench-metric workbench-tone is-compact/g), file).toHaveLength(3)
expect(summary).toContain('v-loading="loading" :aria-busy="loading"')
expect(summary).not.toContain('density-detail-overview')
for (const tone of ['blue', 'green', 'purple']) expect(summary).toContain(`data-tone="${tone}"`)
}
})
it('keeps finance detail tables on one continuous surface', () => {
const statements = read('modules/finance/pages/FinanceCustomerStatementsView.vue')
const arap = read('modules/finance/pages/FinanceCustomerArapView.vue')
expect(statements).not.toContain('<el-tabs')
expect(statements).toContain('title="账单明细表"')
expect(statements).toContain('title="账单支付历史表"')
expect(statements.match(/class="detail-table-scroll workbench-table is-page-surface"/g)).toHaveLength(2)
expect(statements).not.toContain('title="账单明细"')
expect(statements).not.toContain('title="支付历史"')
expect(arap).toContain('<div class="workbench-table is-page-surface"><DataTable :data="operationLogRows"')
const css = read('modules/finance/financeDensity.css')
expect(css).toContain('.finance-density-detail > .detail-page-layout__content .density-form-section')
expect(css).toContain('padding-block: 0;')
const accounts = read('modules/finance/pages/FinanceCustomerAccountsView.vue')
expect(accounts).not.toContain(':deep(.detail-page-layout__content)')
})
it('keeps finance detail insets on the outer page without stripping card or cell padding', () => {
for (const [file, className] of [['FinanceCustomerAccountsView', 'finance-account-detail'], ['FinanceCustomerArapView', 'finance-arap-detail']]) {
expect(read(`modules/finance/pages/${file}.vue`)).toContain(`class="${className} admin-density admin-workbench is-embedded"`)
}
const css = read('modules/finance/financeDensity.css')
const rule = css.slice(css.indexOf('/* The outer AdminPage owns'))
expect(rule).toContain('@media screen')
expect(rule).toContain('padding-inline: 0;')
expect(rule).toContain('.detail-page-layout__content > .settlement-detail-content')
expect(rule).toContain(':is(.density-detail-overview, .statement-summary-section)')
for (const protectedClass of ['workbench-metric', 'el-descriptions__cell', 'query-toolbar', 'el-table']) expect(rule).not.toContain(protectedClass)
const accountStyles = parse(read('modules/finance/pages/FinanceCustomerAccountsView.vue')).descriptor.styles[0]!.content
expect(accountStyles.match(/\.account-metrics \{([^}]+)\}/)![1]).not.toMatch(/padding:|padding-inline:|padding-left:|padding-right:/)
})
it('keeps account detail filters in the shared table layout and long identifiers readable', () => {
const content = read('modules/finance/pages/FinanceCustomerAccountsView.vue')
const header = content.split('<template #header>')[1]!.split('</template>')[0]!
expect(header).toContain('@click="exportLedger"')
expect(header).toContain('v-if="canExport"')
expect(header).toContain(':loading="ledgerExportBusy"')
const ledgerSection = content.split('class="ledger-section density-form-section"')[1]!.split('</FormSection>')[0]!
expect(ledgerSection).not.toContain('#toolbar-actions')
expect(ledgerSection).toContain('class="detail-table-scroll workbench-table is-page-surface"')
expect(ledgerSection).toContain('class="ledger-table" appearance="integrated"')
const rechargeSection = content.split('class="recharge-section density-form-section"')[1]!.split('</FormSection>')[0]!
expect(rechargeSection).toContain('class="detail-table-scroll workbench-table is-page-surface"')
expect(content.match(/account-metric workbench-metric workbench-tone is-compact/g)).toHaveLength(6)
expect(content).toContain('creditExpiryLabel(selected)')
expect(content).toContain("'is-negative': Number(selected.actualBalance) < 0")
expect(content).toContain('<template #filters><QueryToolbar v-model="ledgerFilters"')
expect(content).not.toContain(':deep(.ledger-query.query-toolbar)')
expect(content).toContain('class="density-descriptions account-profile"')
const styles = parse(content).descriptor.styles.map(style => style.content).join('\n')
expect(styles).not.toContain('text-overflow: ellipsis')
expect(styles).toContain('overflow-wrap: anywhere')
})
it('keeps wallet dialogs compact while preserving financial warnings and preview space', () => {
const content = read('modules/finance/pages/FinanceCustomerAccountsView.vue')
const popups = content.slice(content.indexOf('<AppDialog'))
expect(popups.match(/fullscreenable/g)).toHaveLength(5)
expect(popups.match(/class="density-table density-table--editable/g)).toHaveLength(4)
expect(popups.match(/label-position="left" label-width="100px"/g)).toHaveLength(4)
for (const flag of ['refundSaving', 'adjustmentSaving', 'creditLimitSaving', 'reviewSaving']) expect(popups).toContain(`:disabled="${flag}"`)
expect(popups).toContain('data-tone="summary"')
expect(popups).toContain('data-tone="source"')
expect(popups).toContain('data-tone="work"')
expect(popups).toContain('不足部分再使用可用授信')
expect(popups).toContain('次日授信失效')
expect(popups).not.toContain(' readonly')
expect(content).not.toContain('.wallet-form__grid')
expect(content).toContain('max-height: 70vh')
expect(content).toContain('onBeforeUnmount(clearVoucherPreview)')
})
it('keeps notification editors and detail drawers on the shared popup palette', () => {
for (const file of ['MessageComposerDialog', 'TemplateEditorDialog']) {
const content = read(`modules/notifications/components/${file}.vue`)
expect(content).toContain('fullscreenable :confirm-loading="submitting"')
expect(content).toContain('class="density-form density-dialog-sections" label-position="left"')
expect(content).toContain('density-table density-table--editable')
expect(content).toContain('density-actions density-actions--footer')
expect(content).toContain('data-tone="work"')
expect(content).toContain('<InlineHelp')
expect(content).not.toContain('class="dialog-actions"')
expect(content).not.toContain('model-value="站内信" disabled')
}
for (const file of ['PlatformNotificationsView', 'MessageManagementView']) {
const content = read(`modules/notifications/pages/${file}.vue`)
expect(content).toContain('admin-density-drawer--detail')
expect(content).toContain('data-tone="summary"')
expect(content).toContain('data-tone="source"')
expect(content).toContain('@retry="detailRow && openDetail(detailRow)"')
const styles = parse(content).descriptor.styles.map(style => style.content).join('\n')
expect(styles).not.toMatch(/background:|border-left:|font-size:|max-height:/)
}
})
it('keeps container evidence and customs summaries compact without duplicating field styles', () => {
const overview = read('modules/container-loading/components/ContainerOverviewPanel.vue')
const customs = read('modules/container-loading/components/ContainerCustomsPanel.vue')
expect(overview).toContain('class="density-fields pda-summary"')
expect(overview).toContain('height:112px;object-fit:contain')
expect(overview).not.toContain('aspect-ratio:4/3')
expect(customs).toContain('class="density-fields customs-summary"')
expect(customs).not.toContain('class="broker-submission-fact"')
expect(customs).not.toContain('confirmation-history-note')
expect(customs).toContain("document.receiptStatus === 'CONFIRMED'")
})
it('keeps container detail spacing compact and uses shared table styling for dense fulfillment records', () => {
const detail = read('modules/container-loading/pages/ContainerDetailView.vue')
const carrier = read('modules/port-schedules/components/CarrierScheduleQueryPanel.vue')
expect(detail).toContain('<AdminPage spacing="compact" class="container-detail-page admin-density admin-workbench">')
expect(detail).toContain('class="density-element-tabs" @tab-change="rememberWorkbenchTab"')
expect(detail).toContain('<header class="detail-section__heading"><h2 id="container-voyage-heading">船名航次信息</h2><ContainerPortSchedulePanel')
expect(carrier).toContain('class="fulfillment-table density-table"')
expect(carrier).toContain('v-if="dense && fulfillmentEvents.length"')
expect(carrier).toContain('v-else-if="fulfillmentEvents.length" class="fulfillment-timeline"')
expect(carrier).not.toMatch(/\.fulfillment-table\s+[^{}]*\{[^{}]*(?:font-size|background|padding|border):/)
})
})