Files
admin-web/src/styles/adminDensity.test.ts
T
2026-10-21 15:10:54 +08:00

1644 lines
107 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/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/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 = [
...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('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).toMatch(/:not\(\.overseas-transport-editor\)\s*\{\s*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 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 on the continuous cell-editor baseline', () => {
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"')
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')
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-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()
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('data-tone="work"')
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('align-items: start')
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-dialog-sections"')
expect(source).toContain('class="density-table density-table--editable user-editor-table"')
expect(source).toContain('data-tone="work"')
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 drawer sections and existing cell editors without changing other drawers', () => {
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 === 'AppDrawer' && classesOf(node).includes('admin-density-drawer--editor')
if (node.tag === 'AppDrawer') {
editorFound = true
for (const name of ['admin-density', 'admin-density-drawer', 'admin-density-drawer--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.toContain('<FormSection')
expect(source).not.toContain('label-position="top"')
expect(source).toContain('class="density-actions density-actions--footer"')
expect(source).toContain('size="min(620px, 94vw)"')
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('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')
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).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')
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).toContain('data-tone="work"')
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('density-descriptions')
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' ? 7 : 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
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')
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('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 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('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)')
expect(rules.get('.admin-density-dialog .el-form--inline .el-form-item__label')?.['align-items']).toBe('center')
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$/)
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 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-dialog-summary-bg)'], ['declaration', 'var(--admin-dialog-source-bg)'], ['items-section', 'var(--admin-dialog-work-bg)'],
]) {
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('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"')
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")
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('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('density-table density-table--editable dispatch-create-table')
expect(source).not.toContain('<DispatchResourceFields')
expect(source).toContain('<SmartDispatchCargoBoard')
expect(source).toContain('label-position="left" label-width="130px"')
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('class="dispatch-warehouse-picker" aria-label="起运仓"')
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 cargo = read('modules/overseas-warehouse/components/SmartDispatchExistingCargo.vue')
const evidence = read('modules/overseas-warehouse/components/DispatchEvidenceFiles.vue')
expect(workflow).toContain(':fullscreenable="width !== \'560px\'"')
expect(workflow).toContain('<DispatchResourceFields continuous')
expect(workflow).toContain('density-table density-table--editable workflow-editor-table')
expect(workflow).toContain('label-position="left" label-width="130px"')
expect(workflow).toContain(':confirm-loading="busy || uploadingEvidence"')
expect(workflow).not.toContain('aria-label="候选货物分页"')
expect(cargo).toContain('class="existing-cargo density-dialog-sections"')
expect(cargo).toContain('<SmartDispatchCargoBoard')
expect(cargo).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="previewLoading"')
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(/background:|border-left:|font-size:/)
}
})
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('data-tone="work"')
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('data-tone="summary"')
expect(documents).toContain('data-tone="source"')
expect(documents).toContain('data-tone="work"')
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('data-tone="summary"')
expect(list).toContain('data-tone="source"')
expect(list).toContain('data-tone="work"')
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'))
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('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(4)
expect(popups.match(/class="density-table density-table--editable/g)).toHaveLength(3)
expect(popups.match(/label-position="left" label-width="100px"/g)).toHaveLength(3)
for (const flag of ['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).toContain('class="confirmation-history-note"')
})
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):/)
})
})