Files
admin-web/src/styles/adminDensity.test.ts
T

649 lines
42 KiB
TypeScript

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/OceanBookingLinkDialog.vue', 'container-loading/components/ContainerOverviewPanel.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/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/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/PurchaseOrdersView.vue', 'overseas-business/pages/PurchaseOrderDetailView.vue',
'overseas-business/pages/PurchaseOrderEditView.vue', 'overseas-business/pages/TransportOrdersView.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/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/pages/CollectionView.vue']
const selfOperatedConsumers = [...selfOperatedPages, 'self-operated/components/SkuTrend.vue', 'self-operated/pages/OverviewView.vue']
const consumers = [
...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, workbench = 'DOMESTIC'): string[] => node.props.flatMap(prop => {
if (prop.type === 6 && prop.name === 'class') return prop.value?.content.split(/\s+/) ?? []
if (prop.type !== 7 || prop.name !== 'bind' || prop.arg?.type !== 4 || prop.arg.content !== 'class' || prop.exp?.type !== 4 || !prop.exp.content.includes('density')) return []
// Evaluate only the authored density class binding with the shared component's two modes.
const value = new Function('mode', 'isPreload', 'dense', 'isDomestic', 'isOverseas', `return (${prop.exp.content})`)(preload ? 'preload' : 'confirmed', preload, dense, workbench === 'DOMESTIC', workbench === 'OVERSEAS') 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 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('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*[{:.]/)
}
expect(read('modules/overseas-warehouse/pages/ReturnInventoryManagementView.vue')).toContain('class="density-element-tabs density-status-tabs"')
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('class="dashboard-metrics"')
expect(dashboard).toContain('class="dashboard-metric-mark"')
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 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 the supplier license dialog styled at its own teleported root', () => {
const source = read('modules/suppliers/components/SupplierLicenseDialog.vue')
expect(source).toContain('<AppDialog class="admin-density-dialog"')
expect(source).toContain('width="min(560px, calc(100vw - 24px))"')
expect(source).toContain('aria-label="营业执照信息"')
})
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')
for (const className of ['density-detail-header', 'density-detail-heading', 'density-detail-title']) {
expect(domestic).toContain(className)
expect(overseasShell).toContain(className)
}
expect(domestic).toContain('density-detail-overview')
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']) {
const source = read('modules/overseas-warehouse/pages/' + file + '.vue')
const ast = parse(source).descriptor.template!.ast!
let panes = 0
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(node.children.some(child => child.type === 1 && child.tag !== 'template'), 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' ? 6 : 4)
}
})
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
expect(classesOf(root, false, true).some(name => name.startsWith('admin-density')), file).toBe(true)
expect(classesOf(root, false, false).some(name => name.startsWith('admin-density')), file).toBe(false)
}
expect(read('modules/overseas-warehouse/components/OverseasReadOnlyDetail.vue')).toContain(':dense="dense"')
expect(read('modules/overseas-warehouse/pages/InventoryTransactionsView.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
expect(classesOf(root, false, true)).toContain('admin-density')
expect(classesOf(root, false, false)).not.toContain('admin-density')
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) => {
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))
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 && 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)
}
}
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 (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('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 outlined 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('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)
})
expect(rules.get('.admin-density .el-table__body tr.el-table__row--striped td.el-table__cell')?.background).toBe('var(--admin-row-stripe)')
expect(rules.get('.admin-density-dialog .el-descriptions__label.el-descriptions__cell')?.background).toBe('var(--admin-label-bg)')
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$/)
if (desktop && /^(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 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', '#f4fbf8'], ['declaration', '#f7f9ff'], ['items-section', '#fffbf4'],
]) {
expect(sectionStyles.get(`.inbound-measurement-workspace-dialog.inbound-density .measurement-${section}`)).toMatchObject({
padding: 'var(--admin-space-4, 10px) var(--admin-space-5, 12px)',
background,
})
}
// Shared density must not reset the coloured 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')
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('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')
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"')
expect(read('modules/loading-lists/components/PalletLabelPrintDialog.vue')).toContain("'admin-density-dialog': dense")
expect(read('modules/container-loading/components/ContainerSimulationCreate.vue')).toContain("'admin-density-dialog': dense")
for (const file of ['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('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).toContain('class="confirmation-history-note"')
})
})