2026-09-14 17:54:21 +08:00
import { readFileSync } from 'node:fs'
2026-09-15 09:58:15 +08:00
import { dirname , resolve } from 'node:path'
import ts from 'typescript'
2026-09-14 17:54:21 +08:00
import { describe , expect , it } from 'vitest'
import { compileStyle , parse } from 'vue/compiler-sfc'
2026-10-03 04:35:55 +08:00
import { adminNavigation , getNavigationLeaves } from '@/core/navigation/adminNavigation'
2026-09-14 17:54:21 +08:00
// Approved entry/consumer inventory. Do not discover unrelated menus recursively.
2026-09-14 18:34:34 +08:00
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' ,
2026-10-20 10:11:31 +08:00
'container-loading/components/SmartLoadingDialog.vue' , 'container-loading/pages/GlobalSmartLoadingView.vue' ,
'loading-lists/pages/PreloadPlanDetailView.vue' ,
2026-09-14 18:34:34 +08:00
'container-loading/components/OceanBookingLinkDialog.vue' , 'container-loading/components/ContainerOverviewPanel.vue' ,
2026-10-20 10:11:31 +08:00
'port-schedules/components/ContainerPortSchedulePanel.vue' ,
2026-09-14 18:34:34 +08:00
'container-loading/components/ContainerCustomsPanel.vue' , 'container-loading/components/ContainerLastLegFeesPanel.vue' ,
'loading-lists/components/ContainerFeesPanel.vue' , 'port-schedules/components/CarrierScheduleQueryPanel.vue' ,
]
2026-09-14 20:03:27 +08:00
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' ,
2026-10-21 16:12:24 +08:00
'overseas-warehouse/components/TransportExecutionPanel.vue' ,
2026-09-14 20:03:27 +08:00
'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' ,
2026-10-20 10:11:31 +08:00
'overseas-warehouse/pages/LocationsView.vue' ,
'overseas-warehouse/pages/CargoDiscardsView.vue' ,
2026-09-14 20:03:27 +08:00
'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' ,
]
2026-09-15 09:58:15 +08:00
const overseasBusinessConsumers = [
2026-10-26 13:43:08 +08:00
'overseas-business/pages/OverseasFeesView.vue' ,
2026-10-21 14:30:33 +08:00
'overseas-business/components/TransportDemandForm.vue' ,
2026-09-15 09:58:15 +08:00
'overseas-business/pages/PurchaseOrdersView.vue' , 'overseas-business/pages/PurchaseOrderDetailView.vue' ,
'overseas-business/pages/PurchaseOrderEditView.vue' , 'overseas-business/pages/TransportOrdersView.vue' ,
2026-10-20 11:54:36 +08:00
'overseas-business/pages/TruckOrderDetailView.vue' ,
2026-10-10 02:19:39 +08:00
'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' ,
2026-09-15 09:58:15 +08:00
]
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' ,
2026-10-09 17:36:32 +08:00
'overseas-master-data/components/TransportRouteDialog.vue' ,
2026-10-03 04:35:55 +08:00
'overseas-master-data/pages/DriversView.vue' ,
'overseas-master-data/pages/TripsView.vue' ,
2026-10-07 16:46:57 +08:00
'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' ,
2026-10-07 12:24:21 +08:00
'overseas-master-data/pages/TripOperationsView.vue' ,
2026-10-19 17:56:31 +08:00
'overseas-master-data/components/TripOperationsCharts.vue' ,
2026-10-03 04:35:55 +08:00
'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' ,
2026-09-15 09:58:15 +08:00
'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' ,
]
2026-09-15 10:38:03 +08:00
const systemConsumers = [ 'iam/pages/InternalUsersView.vue' , 'iam/pages/RolesView.vue' , 'iam/pages/PermissionsView.vue' ]
2026-10-20 23:34:11 +08:00
const selfOperatedPages = [ 'self-operated/components/AnalyticsView.vue' , 'self-operated/components/InventoryTurnoverView.vue' ]
2026-10-13 00:45:41 +08:00
const selfOperatedConsumers = [... selfOperatedPages , 'self-operated/components/SkuTrend.vue' , 'self-operated/pages/OverviewView.vue' ]
2026-09-14 17:54:21 +08:00
const consumers = [
2026-10-26 13:43:08 +08:00
'overseas-business/components/OverseasFeeEditor.vue' , 'overseas-business/components/StorageRentPanel.vue' ,
2026-10-12 20:33:39 +08:00
... selfOperatedConsumers ,
2026-10-07 16:46:57 +08:00
'overseas-business/components/FleetArrangementSummary.vue' ,
2026-09-15 09:58:15 +08:00
... notificationConsumers ,
... systemConsumers ,
... generalCatalogConsumers ,
... financeConsumers ,
... customerServiceConsumers ,
... overseasBusinessConsumers ,
2026-09-14 20:03:27 +08:00
... overseasConsumers ,
2026-09-14 18:34:34 +08:00
... cargoConsumers ,
2026-09-14 17:54:21 +08:00
'customers/pages/CustomersView.vue' , 'customers/pages/CustomerDetailView.vue' ,
'customers/pages/OverseasStorageFeesView.vue' , 'customers/components/CustomerBillingRulesPanel.vue' ,
2026-09-15 09:58:15 +08:00
'unified-orders/components/InboundFeeDetailManager.vue' , 'unified-orders/components/OrderDeclarationEditorDialog.vue' ,
2026-09-14 17:54:21 +08:00
'customers/CustomerOverseasStorageFeePanel.vue' , 'unified-orders/pages/UnifiedOrdersView.vue' ,
2026-09-15 10:19:05 +08:00
'inbounds/core/DropshipPalletPrintPanel.vue' ,
2026-09-14 17:54:21 +08:00
'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' ,
]
2026-09-15 09:58:15 +08:00
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' ]
2026-09-14 17:54:21 +08:00
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' },
2026-09-28 15:24:33 +08:00
{ title : '船期日历' , path : '/sailing-calendar' , file : 'sailing-calendar/pages/SailingCalendarView.vue' },
2026-09-14 17:54:21 +08:00
]
2026-09-14 18:34:34 +08:00
const customerPages = [
{ title : '客户列表' , path : '/customers' , file : 'customers/pages/CustomersView.vue' },
{ title : '海外仓阶梯费用' , path : '/customers/overseas-storage-fees' , file : 'customers/pages/OverseasStorageFeesView.vue' },
]
2026-09-14 17:54:21 +08:00
const densityPages = [... new Set ([
2026-10-12 20:33:39 +08:00
... selfOperatedPages ,
2026-09-15 09:58:15 +08:00
... notificationConsumers . filter ( file => file . includes ( '/pages/' )),
... systemConsumers ,
... generalCatalogConsumers . filter ( file => file . includes ( '/pages/' )),
... financeConsumers ,
... customerServiceConsumers ,
... overseasBusinessConsumers ,
2026-09-14 20:03:27 +08:00
... overseasConsumers . filter ( file => ! file . endsWith ( 'PalletDetailView.vue' )),
2026-09-14 18:34:34 +08:00
... cargoConsumers . filter ( file => file . includes ( '/pages/' ) || [ 'ContainerOverviewPanel.vue' , 'ContainerCustomsPanel.vue' , 'ContainerLastLegFeesPanel.vue' , 'ContainerFeesPanel.vue' , 'OceanBookingDocumentsPanel.vue' ]. some ( name => file . endsWith ( name ))),
2026-09-14 17:54:21 +08:00
... domesticPages . map ( page => page . file ), 'loading-lists/pages/PreloadPlanDetailView.vue' ,
'loading-lists/components/LoadingListDetailDrawer.vue' , 'domestic-containers/pages/DomesticContainerInventoryDetailView.vue' ,
2026-09-14 18:34:34 +08:00
... customerPages . map ( page => page . file ), 'customers/pages/CustomerDetailView.vue' , 'audit/components/CustomerAuditLogsPanel.vue' ,
2026-09-14 17:54:21 +08:00
])]
type TemplateNode = NonNullable < NonNullable < ReturnType < typeof parse > [ 'descriptor' ][ 'template' ] > [ 'ast' ] >[ 'children' ][ number ]
type ElementNode = Extract < TemplateNode , { type : 1 }>
2026-10-20 23:34:11 +08:00
const classesOf = ( node : ElementNode , preload = true , dense = preload , region = 'DOMESTIC' , workbench = false , embedded = false ) : string [] => node . props . flatMap ( prop => {
2026-09-14 17:54:21 +08:00
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 []
2026-10-20 23:34:11 +08:00
// 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 >
2026-09-14 18:34:34 +08:00
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 )
2026-09-14 17:54:21 +08:00
})
2026-10-20 23:34:11 +08:00
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
}
2026-09-14 17:54:21 +08:00
const styles = [
2026-09-15 09:58:15 +08:00
... parse ( read ( 'shared/components/BatchImportDialog.vue' )). descriptor . styles . map ( style => ({ file : 'BatchImportDialog.vue' , css : style.content })),
2026-09-14 17:54:21 +08:00
... 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' , () => {
2026-10-26 13:58:52 +08:00
it ( 'keeps pallet overview spacing and metric appearance owned by shared primitives' , () => {
for ( const file of [ 'warehouse-resources/pallets/pages/PalletsView.vue' , 'overseas-warehouse/pages/PalletsView.vue' ]) {
const page = parse ( read ( `modules/ ${ file } ` )). descriptor
const css = page . styles . map ( style => style . content ). join ( '\n' )
expect ( page . template ! . content , file ). toMatch ( /<AdminPage\b[^>]*spacing="compact"/ )
expect ( css , file ). toContain ( '.pallet-page.admin-page.is-spacing-compact { gap: var(--admin-space-3); }' )
expect ( page . template ! . content , file ). toContain ( 'workbench-metric workbench-tone is-compact' )
expect ( css , file ). not . toMatch ( /\.summary-grid\s+(?:article|span|strong|\.idle|\.occupied)\b/ )
expect ( css , file ). not . toMatch ( /\.workbench-metric[\s.{]/ )
}
2026-10-26 11:34:22 +08:00
})
2026-10-21 14:30:33 +08:00
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))' )
})
2026-10-21 13:22:42 +08:00
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)' )
})
2026-10-20 23:34:11 +08:00
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' )
})
2026-10-22 11:56:54 +08:00
it ( 'outlines opted-in customer forms without overriding native focus, validation or disabled styles' , () => {
const rules = new Map < string , Record < string , string >>()
cssRoot ( shared ). walkRules ( rule => {
const values : Record < string , string > = {}
rule . walkDecls ( decl => { values [ decl . prop ] = decl . value })
rules . set ( rule . selector , values )
})
expect ( rules . get ( ':is(.admin-density, .admin-density-dialog) .density-form--outlined :is(.el-input, .el-textarea)' )). toEqual ({
'--el-input-border-color' : 'var(--el-border-color)' ,
'--el-input-hover-border-color' : 'var(--el-border-color-hover)' ,
'--el-input-bg-color' : 'var(--admin-surface)' ,
})
2026-10-26 13:58:52 +08:00
expect ( rules . get ( ':is(.admin-density, .admin-density-dialog) .density-form--outlined' )). toEqual ({
'--el-border-radius-base' : 'var(--cc-radius-lg)' ,
})
2026-10-22 11:56:54 +08:00
const customerRules = new Map < string , Record < string , string >>()
cssRoot ( read ( 'modules/customers/customerDensity.css' )). walkRules ( rule => {
const values : Record < string , string > = {}
rule . walkDecls ( decl => { values [ decl . prop ] = decl . value })
customerRules . set ( rule . selector , values )
})
expect ( customerRules . get ( '.customer-editor-dialog .density-form--outlined .el-input-number .el-input__inner' )). toEqual ({ 'text-align' : 'left' })
})
2026-10-20 16:45:09 +08:00
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' ])
})
2026-10-20 15:48:23 +08:00
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%' )
})
})
}
})
2026-10-26 13:58:52 +08:00
it ( 'keeps declaration order fields outlined within the continuous cell-editor layout' , () => {
2026-10-20 14:25:01 +08:00
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 )
2026-10-26 13:58:52 +08:00
expect ( orderFields ). toContain ( 'class="order-fields density-form density-form--outlined"' )
2026-10-20 14:25:01 +08:00
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"' )
})
2026-10-20 10:11:31 +08:00
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' )
})
2026-10-16 18:25:49 +08:00
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' )
2026-10-20 23:34:11 +08:00
const [ frame ] = expectWorkbenchDialogs ( dialog )
expect ( classesOf ( frame ! )). toContain ( 'density-confirm-dialog' )
2026-10-16 18:25:49 +08:00
expect ( dialog ). toContain ( 'align-center' )
expect ( css ). toContain ( '.admin-density-dialog.density-confirm-dialog.el-dialog:not(.is-fullscreen) { margin: auto; }' )
2026-10-21 07:40:08 +08:00
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;' )
2026-10-16 18:25:49 +08:00
})
2026-10-19 12:20:13 +08:00
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 ) => {
2026-10-15 19:23:16 +08:00
const values : Record < string , string > = {}
2026-10-19 12:20:13 +08:00
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 })
})
2026-10-15 19:23:16 +08:00
return values
}
2026-10-19 12:20:13 +08:00
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)' )
2026-10-19 17:02:27 +08:00
expect ( panel . template ! . content ). toContain ( 'v-if="canCreate"' )
expect ( panel . template ! . content ). toContain ( '新增预约' )
2026-10-15 19:23:16 +08:00
})
2026-10-20 10:11:31 +08:00
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' )
2026-10-20 23:34:11 +08:00
expect ( view . template ! . content ). toContain ( 'label-position="left" label-width="150px"' )
expect ( view . template ! . content ). toContain ( '<el-date-picker v-model="plannedAt" type="datetime"' )
2026-10-20 10:11:31 +08:00
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 )
})
2026-10-03 04:35:55 +08:00
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' ,
2026-10-12 11:45:00 +08:00
'appointment-calendar/AppointmentCargoTabs.vue' ,
2026-10-03 04:35:55 +08:00
]) {
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*[{:.]/ )
}
2026-10-20 23:34:11 +08:00
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 )
2026-10-03 04:35:55 +08:00
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 )
}
})
2026-09-15 10:38:03 +08:00
it ( 'keeps dashboard visualizations outside detail-field table styling' , () => {
const dashboard = read ( 'modules/dashboard/pages/DashboardView.vue' )
2026-10-20 23:34:11 +08:00
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 )
2026-09-15 10:38:03 +08:00
expect ( dashboard ). not . toContain ( 'density-fields' )
})
2026-10-13 00:45:41 +08:00
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" )
2026-10-20 23:34:11 +08:00
expect ( source ). toContain ( '<AdminPage class="admin-density admin-workbench dashboard-page">' )
2026-10-13 00:45:41 +08:00
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' )
})
2026-09-15 09:58:15 +08:00
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 )
}
})
2026-10-20 10:11:31 +08:00
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' )
2026-10-20 23:34:11 +08:00
const importer = expectWorkbenchDialogs ( source ). find ( node => classesOf ( node ). includes ( 'warehouse-import-dialog' ))
expect ( importer ). toBeDefined ()
2026-10-20 10:11:31 +08:00
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>' ))
2026-10-20 23:34:11 +08:00
expectWorkbenchDialogs ( source )
2026-10-20 10:11:31 +08:00
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>' ))
2026-10-20 23:34:11 +08:00
expectWorkbenchDialogs ( list )
2026-10-20 10:11:31 +08:00
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 ]) {
2026-10-20 23:34:11 +08:00
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 )
2026-10-20 10:11:31 +08:00
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"' )
2026-10-20 23:34:11 +08:00
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' )
2026-10-20 10:11:31 +08:00
})
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 ]) {
2026-10-20 23:34:11 +08:00
const dialogs = expectWorkbenchDialogs ( source )
2026-10-20 10:11:31 +08:00
expect ( dialogs . length ). toBeGreaterThan ( 0 )
for ( const dialog of dialogs ) {
2026-10-20 23:34:11 +08:00
expect ( dialog . loc . source ). not . toContain ( 'label-position="top"' )
expect ( dialog . loc . source ). not . toContain ( '#header' )
2026-10-20 10:11:31 +08:00
}
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/ )
})
2026-09-15 09:58:15 +08:00
it ( 'keeps the supplier license dialog styled at its own teleported root' , () => {
const source = read ( 'modules/suppliers/components/SupplierLicenseDialog.vue' )
2026-10-20 23:34:11 +08:00
expectWorkbenchDialogs ( source )
2026-09-15 09:58:15 +08:00
expect ( source ). toContain ( 'width="min(560px, calc(100vw - 24px))"' )
expect ( source ). toContain ( 'aria-label="营业执照信息"' )
2026-10-20 23:34:11 +08:00
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 )
2026-10-20 10:11:31 +08:00
expect ( source ). toContain ( 'class="density-table license-info-table"' )
expect ( source ). not . toContain ( 'license-card' )
expect ( source ). not . toContain ( 'box-shadow' )
2026-09-15 09:58:15 +08:00
})
2026-09-14 20:03:27 +08:00
2026-09-14 20:21:02 +08:00
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' )
2026-10-21 14:30:33 +08:00
const truckDetail = read ( 'modules/overseas-business/pages/TruckOrderDetailView.vue' )
2026-09-14 20:21:02 +08:00
for ( const className of [ 'density-detail-header' , 'density-detail-heading' , 'density-detail-title' ]) {
expect ( domestic ). toContain ( className )
expect ( overseasShell ). toContain ( className )
2026-10-21 14:30:33 +08:00
expect ( truckDetail ). toContain ( className )
2026-09-14 20:21:02 +08:00
}
2026-10-21 14:30:33 +08:00
expect ( truckDetail ). toContain ( 'density-detail-layout' )
expect ( truckDetail ). toContain ( 'density-descriptions' )
2026-10-26 15:51:07 +08:00
expect ( domestic ). toContain ( '<DetailPageLayout' )
expect ( domestic ). toContain ( 'order-info-grid density-fields' )
2026-09-14 20:21:02 +08:00
expect ( read ( 'modules/overseas-warehouse/pages/LastMileOrderDetailView.vue' )). toContain ( 'density-detail-overview' )
})
2026-10-03 04:35:55 +08:00
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 )))
2026-09-14 20:03:27 +08:00
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; }' )
2026-10-21 16:40:10 +08:00
for ( const file of [ 'LastMileOrderDetailView' , 'DeliveryOrderDetailView' , 'DispatchDetailView' ]) {
2026-09-14 20:03:27 +08:00
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 )
2026-10-19 12:20:13 +08:00
expect ( panes ). toBe ( file === 'LastMileOrderDetailView' ? 7 : 4 )
2026-09-14 20:03:27 +08:00
}
})
2026-10-21 17:35:43 +08:00
it ( 'fits the five dispatch vehicle fields in one desktop row with read-only detail tabs' , () => {
2026-10-21 16:40:10 +08:00
const source = read ( 'modules/overseas-warehouse/pages/DispatchDetailView.vue' )
const css = parse ( source ). descriptor . styles . map ( style => style . content ). join ( '\n' )
const declarations = ( source : string , selector : string ) => {
const values : Record < string , string > = {}
cssRoot ( source ). walkRules ( rule => {
if ( rule . selector === selector && rule . parent ? . type !== 'atrule' ) rule . walkDecls ( decl => { values [ decl . prop ] = decl . value })
})
return values
}
expect ( declarations ( css , '.dispatch-detail .vehicle-fields' )[ 'grid-template-columns' ]). toBe ( 'minmax(0, 1.3fr) minmax(0, 1.6fr) minmax(0, 1.2fr) minmax(0, 1.5fr) minmax(0, 0.8fr)' )
expect ( declarations ( css , '.dispatch-detail .vehicle-fields > div' )[ 'grid-template-columns' ]). toBe ( 'max-content minmax(0, 1fr)' )
2026-10-21 17:35:43 +08:00
expect ( css ). not . toContain ( '.dispatch-actions' )
2026-10-21 16:40:10 +08:00
expect ( declarations ( shared , '.admin-density .density-contained-tabs > .el-tabs__content' ). padding ). toBe ( 'var(--admin-space-5)' )
expect ( declarations ( shared , '.admin-density .density-contained-tabs.density-contained-tabs--flush > .el-tabs__content' ). padding ). toBe ( '0' )
expect ( elementsOf ( source , 'el-tabs' ). every ( node => classesOf ( node ). includes ( 'density-contained-tabs--flush' ))). toBe ( true )
})
2026-09-14 20:03:27 +08:00
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
2026-10-20 23:34:11 +08:00
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 )
}
}
2026-09-14 20:03:27 +08:00
}
expect ( read ( 'modules/overseas-warehouse/components/OverseasReadOnlyDetail.vue' )). toContain ( ':dense="dense"' )
2026-10-20 23:34:11 +08:00
expect ( read ( 'modules/overseas-warehouse/components/OverseasReadOnlyDetail.vue' )). toContain ( ':workbench="workbench"' )
2026-09-14 20:03:27 +08:00
expect ( read ( 'modules/overseas-warehouse/pages/InventoryTransactionsView.vue' )). toContain ( '<InboundCodeRelationshipDialog dense' )
2026-10-20 10:11:31 +08:00
expect ( read ( 'modules/inbounds/pages/InboundOrdersView.vue' )). toContain ( '<InboundCodeRelationshipDialog dense' )
2026-09-14 20:03:27 +08:00
for ( const file of [ 'CartonDetailView' , 'ProductDetailView' ]) expect ( read ( 'modules/overseas-warehouse/pages/' + file + '.vue' )). not . toContain ( ' dense' )
})
2026-10-08 11:16:58 +08:00
it ( 'covers five cargo menus, independent detail routes and both workbench modes' , () => {
2026-09-14 18:34:34 +08:00
const navigation = read ( 'core/navigation/adminNavigation.ts' )
2026-10-08 11:16:58 +08:00
for ( const path of [ '/ocean-bookings' , '/trucking' , '/container-loading/domestic' , '/loading-lists' , '/loading-plans' ]) expect ( navigation ). toContain ( `path: ' ${ path } '` )
2026-09-14 18:34:34 +08:00
for ( const [ prefix , workbench ] of [[ 'Domestic' , 'DOMESTIC' ], [ 'Overseas' , 'OVERSEAS' ]]) {
for ( const [ suffix , component ] of [[ 'ContainersView' , 'ContainersView' ], [ 'ContainerDetailView' , 'ContainerDetailView' ]]) {
2026-10-08 11:16:58 +08:00
expect ( read ( `modules/container-loading/pages/ ${ prefix }${ suffix } .vue` )). toContain ( suffix === 'ContainersView' ? `<ContainerWorkbenchView stage=" ${ workbench } "` : `< ${ component } workbench=" ${ workbench } "` )
2026-09-14 18:34:34 +08:00
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
2026-10-20 23:34:11 +08:00
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 )
}
}
2026-09-14 18:34:34 +08:00
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 !
2026-10-20 10:11:31 +08:00
const visit = ( nodes : typeof ast . children , inCell = false , insideDensity = false , insideEditor = false ) => {
2026-09-14 18:34:34 +08:00
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 ))
2026-10-20 10:11:31 +08:00
const editable = insideEditor || classes . includes ( 'density-table--editable' )
2026-09-14 18:34:34 +08:00
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' )
2026-10-20 10:11:31 +08:00
if ( inCell && ! editable && node . tag === 'el-button' ) {
2026-09-14 18:34:34 +08:00
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 )
}
2026-10-20 10:11:31 +08:00
visit ( node . children , inCell || node . tag === 'td' , enabled , editable )
2026-09-14 18:34:34 +08:00
}
}
visit ( ast . children )
}
})
2026-09-14 17:54:21 +08:00
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 )
2026-10-16 18:25:49 +08:00
if ( classes . includes ( 'density-confirm-dialog' )) {
expect ( classes , ` ${ file } : confirmation density` ). toContain ( 'admin-density-dialog' )
checked += 1
}
2026-10-10 02:19:39 +08:00
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 ))) {
2026-09-14 17:54:21 +08:00
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 )
}
})
2026-10-03 04:35:55 +08:00
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 )))
2026-09-14 17:54:21 +08:00
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' )
})
2026-10-20 10:11:31 +08:00
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' )
2026-10-21 17:49:04 +08:00
const editor = customers . split ( '<AppDialog v-model="editorOpen"' )[ 1 ] ! . split ( '</AppDialog>' )[ 0 ] !
const sections = [... editor . matchAll ( /<section\b[^>]*>/g )]. map ( match => match [ 0 ])
expect ( sections ). toHaveLength ( 3 )
for ( const section of sections ) {
expect ( section ). toContain ( 'density-dialog-section' )
expect ( section ). not . toContain ( 'data-tone=' )
}
2026-10-20 10:11:31 +08:00
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' )
2026-10-21 17:56:57 +08:00
expect ( source , file ). toContain ( file === 'customers/pages/OverseasStorageFeesView.vue' ? 'class="density-dialog-section"' : 'class="density-dialog-section" data-tone="work"' )
2026-10-20 10:11:31 +08:00
expect ( source , file ). toContain ( 'fullscreenable' )
expect ( source , file ). not . toContain ( 'class="dialog-actions"' )
}
})
2026-09-14 18:34:34 +08:00
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"' )
})
2026-09-14 17:54:21 +08:00
it ( 'uses shared outlined actions across domestic tables and explicit classes on teleported dialogs' , () => {
2026-09-14 18:34:34 +08:00
for ( const file of [... new Set ([... densityPages , ... consumers ])]) {
2026-09-14 17:54:21 +08:00
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' ))
2026-10-12 20:33:39 +08:00
if ( densityPages . includes ( file ) && ! selfOperatedPages . includes ( file ) && isActionSlot && node . tag === 'el-button' ) expect ( classesOf ( node ), ` ${ file } : row action` ). toContain ( 'density-action' )
2026-09-14 17:54:21 +08:00
if ([ 'AppDialog' , 'el-dialog' ]. includes ( node . tag )) expect ( classesOf ( node ), ` ${ file } : teleported dialog` ). toContain ( 'admin-density-dialog' )
2026-09-14 18:34:34 +08:00
if ([ 'AppDrawer' , 'el-drawer' ]. includes ( node . tag )) expect ( classesOf ( node ), ` ${ file } : teleported drawer` ). toContain ( 'admin-density-drawer' )
2026-09-14 17:54:21 +08:00
visit ( node . children , isActionSlot , node . tag )
}
}
visit ( template . children )
}
})
2026-10-13 17:29:45 +08:00
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' )
2026-10-20 23:34:11 +08:00
expect ( page ). toContain ( 'class="admin-density admin-workbench"' )
2026-10-13 17:29:45 +08:00
expect ( page ). toContain ( '@/styles/adminDensity.css' )
2026-10-20 23:34:11 +08:00
expect ( page ). toContain ( '@/styles/adminWorkbench.css' )
2026-10-13 17:29:45 +08:00
expect ( page ). toContain ( '<CollectionAccountMonitor' )
expect ( page ). not . toContain ( '<DataTable' )
2026-10-20 23:34:11 +08:00
expect ( monitor ). toContain ( 'workbench-panel' )
2026-10-13 17:29:45 +08:00
expect ( monitor ). not . toMatch ( /--el-[\w-]+\s*:/ )
})
2026-10-12 20:33:39 +08:00
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 )
}
})
2026-09-14 17:54:21 +08:00
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' )
}
})
2026-09-14 18:34:34 +08:00
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 )
})
2026-10-21 13:22:42 +08:00
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 )
2026-09-14 18:34:34 +08:00
expect ( rules . get ( '.admin-density-dialog .el-descriptions__label.el-descriptions__cell' ) ? . background ). toBe ( 'var(--admin-label-bg)' )
2026-10-21 17:26:49 +08:00
const horizontalLabelSelector = '.admin-density-dialog .el-form-item:not(.el-form-item--label-top) .el-form-item__label'
expect ( rules . get ( horizontalLabelSelector ) ? .[ 'align-items' ]). toBe ( 'center' )
const labelFixture = document . createElement ( 'div' )
labelFixture . innerHTML = '<div class="admin-density-dialog"><form class="el-form"><div class="el-form-item el-form-item--label-left"><label class="el-form-item__label">计划送仓日期</label></div></form></div>'
const formItem = labelFixture . querySelector ( '.el-form-item' ) !
const label = labelFixture . querySelector ( 'label' ) !
expect ( label . matches ( horizontalLabelSelector )). toBe ( true )
formItem . className = 'el-form-item el-form-item--label-right'
expect ( label . matches ( horizontalLabelSelector )). toBe ( true )
formItem . className = 'el-form-item el-form-item--label-top'
expect ( label . matches ( horizontalLabelSelector )). toBe ( false )
2026-10-20 23:34:11 +08:00
expect ( rules . get ( '.admin-density-dialog.el-dialog.admin-density-workspace > .el-dialog__body' ) ? . flex ). toBe ( '1 1 auto' )
2026-09-14 18:34:34 +08:00
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)' )
})
2026-09-14 17:54:21 +08:00
it ( 'prevents page copies of action paint, tab primitives and desktop density dimensions' , () => {
for ( const { file , css } of styles ) cssRoot ( css ). walkRules ( rule => {
2026-09-15 09:58:15 +08:00
expect ( rule . selector . trim (), file + ': empty selector' ). not . toBe ( '' )
2026-09-14 17:54:21 +08:00
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$/ )
2026-10-26 15:51:07 +08:00
// Decorative artwork is not a form control or data row.
const decorativeBrand = file === 'ocean-bookings/pages/OceanBookingDetailView.vue' && rule . selector === '.booking-brand'
if ( desktop && ! decorativeBrand && /^(height|min-height)$/ . test ( decl . prop )) expect ( decl . value , ` ${ file } : ${ rule . selector } ` ). not . toMatch ( /^(28|32|36|38|64)px$/ )
2026-09-14 17:54:21 +08:00
})
})
})
2026-10-08 17:08:03 +08:00
it ( 'keeps measurement sections within the clipped panel and gives long references room' , () => {
const dialog = parse ( read ( 'modules/inbounds/measurement/InboundMeasurementDialog.vue' )). descriptor
2026-10-19 17:56:31 +08:00
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)' ,
})
2026-10-08 17:08:03 +08:00
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 [
2026-10-21 23:20:10 +08:00
[ 'summary-section' , 'var(--admin-surface, #fff)' ], [ 'declaration' , 'var(--admin-dialog-source-bg)' ], [ 'items-section' , 'var(--admin-surface, #fff)' ],
2026-10-08 17:08:03 +08:00
]) {
expect ( sectionStyles . get ( `.inbound-measurement-workspace-dialog.inbound-density .measurement- ${ section } ` )). toMatchObject ({
padding : 'var(--admin-space-4, 10px) var(--admin-space-5, 12px)' ,
background ,
})
}
2026-10-21 17:49:04 +08:00
expect ( sectionStyles . get ( '.inbound-measurement-workspace-dialog.inbound-density .measurement-deviation-alert' ) ? . background ). toBe ( 'var(--admin-surface, #fff)' )
// Shared density must not reset the sections' horizontal gutters.
2026-10-08 17:08:03 +08:00
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/ )
}
})
2026-09-15 10:38:03 +08:00
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' )
2026-10-26 13:43:08 +08:00
expect ( tableCss ). toContain ( '.data-table > .data-table__toolbar.has-inline-filters' )
expect ( tableCss ). toContain ( '.data-table__toolbar.has-inline-filters > .data-table__filters { flex: 1 1 0; min-width: 0; }' )
2026-09-15 10:38:03 +08:00
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)$/ )
})
})
})
2026-10-20 17:59:13 +08:00
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; }' )
2026-10-21 07:53:28 +08:00
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' )
2026-10-20 17:59:13 +08:00
})
2026-09-14 17:54:21 +08:00
it ( 'keeps conditional shared consumers opt-in instead of enabling other menus' , () => {
2026-09-30 14:01:58 +08:00
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' )
2026-10-21 15:05:56 +08:00
expect ( read ( 'modules/overseas-master-data/pages/OwnedBusinessView.vue' )). toContain ( 'class="row-actions density-actions admin-text-actions"' )
2026-09-30 14:01:58 +08:00
const dispatches = read ( 'modules/overseas-warehouse/pages/DispatchesView.vue' )
expect ( dispatches ). toContain ( 'appearance="integrated"' )
2026-10-08 14:11:54 +08:00
expect ( dispatches ). toContain ( '<template #filters><div class="integrated-query-group">' )
expect ( dispatches ). toContain ( 'class="density-status-tabs" role="tablist"' )
2026-09-30 14:01:58 +08:00
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"' )
2026-09-14 18:34:34 +08:00
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;' )
2026-09-30 14:01:58 +08:00
expect ( read ( 'modules/loading-lists/components/LoadingListsView.vue' )). toContain ( 'class="business-page loading-lists-page admin-density"' )
2026-09-14 17:54:21 +08:00
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" )
2026-10-20 23:49:47 +08:00
expect ( read ( 'modules/container-loading/components/ContainerSimulationCreate.vue' )). toContain ( 'width="min(800px, calc(100vw - 32px))"' )
2026-09-30 14:01:58 +08:00
for ( const file of [ 'loading-lists/components/LoadingListDetailDrawer.vue' , 'loading-lists/components/PalletLabelPrintDialog.vue' , 'container-loading/components/ContainerSimulationCreate.vue' ]) {
2026-09-14 18:34:34 +08:00
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 )
}
2026-09-14 17:54:21 +08:00
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' )
}
})
2026-10-19 17:56:31 +08:00
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))' ,
])
})
2026-10-20 10:11:31 +08:00
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' )
2026-10-26 13:43:08 +08:00
expect ( detail ). toContain ( '<section class="density-dialog-section" aria-label="尾程费用登记">' )
2026-10-20 10:11:31 +08:00
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"' )
})
2026-10-21 13:22:42 +08:00
it ( 'keeps dispatch planning on shared sections and cargo selection in the three-column board' , () => {
2026-10-20 10:11:31 +08:00
const source = read ( 'modules/overseas-warehouse/components/SmartDispatchDialog.vue' )
2026-10-21 13:22:42 +08:00
const board = read ( 'modules/overseas-warehouse/components/SmartDispatchCargoBoard.vue' )
2026-10-20 10:11:31 +08:00
const fields = read ( 'modules/overseas-warehouse/components/DispatchResourceFields.vue' )
expect ( source ). toContain ( 'fullscreenable' )
2026-10-22 23:10:09 +08:00
expect ( source ). toContain ( 'aria-label="排车设置"' )
2026-10-21 13:22:42 +08:00
expect ( source ). not . toContain ( '<DispatchResourceFields' )
expect ( source ). toContain ( '<SmartDispatchCargoBoard' )
2026-10-22 23:10:09 +08:00
expect ( source ). toContain ( 'label-position="left" label-width="70px"' )
2026-10-21 14:30:33 +08:00
expect ( source ). toContain ( 'class="dispatch-context-toolbar"' )
expect ( source ). not . toContain ( 'class="density-dialog-section dispatch-draft-toolbar"' )
expect ( source ). toContain ( '<template #vehicle-controls>' )
2026-10-20 23:34:11 +08:00
expect ( source ). toContain ( 'class="density-form dispatch-setup-fields"' )
2026-10-21 16:40:10 +08:00
expect ( source ). toContain ( '<el-form-item v-if="mode === \'empty\'" label="起运仓">' )
expect ( source ). toContain ( '<template #order-controls>' )
2026-10-21 15:10:54 +08:00
expect ( source ). not . toContain ( 'class="dispatch-warehouse-field" label="起运仓"' )
2026-10-20 23:34:11 +08:00
expect ( source ). not . toContain ( 'data-tone="work"' )
expect ( source ). not . toContain ( 'data-tone="summary"' )
expect ( source ). not . toContain ( 'data-tone="source"' )
2026-10-21 13:22:42 +08:00
expect ( board ). toContain ( 'class="dispatch-cargo-board"' )
expect ( board ). toContain ( 'class="density-table cargo-board-table"' )
2026-10-21 14:30:33 +08:00
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' )
2026-10-21 15:10:54 +08:00
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">' )
2026-10-21 14:30:33 +08:00
expect ( source ). not . toContain ( 'admin-density-workspace' )
2026-10-21 13:22:42 +08:00
expect ( board ). toContain ( 'aria-label="待排订单"' )
expect ( board ). toContain ( 'aria-label="托盘明细"' )
expect ( board ). toContain ( 'aria-label="车辆装载"' )
expect ( board ). toContain ( 'class="cargo-board-group-details"' )
2026-10-20 10:11:31 +08:00
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"' )
})
2026-10-21 14:30:33 +08:00
it ( 'keeps dispatch workflow and owned evidence on shared compact sections' , () => {
2026-10-20 10:11:31 +08:00
const workflow = read ( 'modules/overseas-warehouse/components/DispatchWorkflowDialog.vue' )
2026-10-21 23:20:10 +08:00
const maintenance = read ( 'modules/overseas-warehouse/components/DispatchVehicleMaintenanceForm.vue' )
2026-10-20 10:11:31 +08:00
const evidence = read ( 'modules/overseas-warehouse/components/DispatchEvidenceFiles.vue' )
expect ( workflow ). toContain ( ':fullscreenable="width !== \'560px\'"' )
2026-10-21 23:20:10 +08:00
expect ( workflow ). toContain ( '<DispatchVehicleMaintenanceForm' )
2026-10-22 23:10:09 +08:00
expect ( maintenance ). toMatch ( /<DispatchResourceFields\b[^>]*\bcontinuous\b/ )
2026-10-21 23:20:10 +08:00
expect ( maintenance ). toContain ( 'density-table density-table--editable maintenance-table' )
2026-10-20 10:11:31 +08:00
expect ( workflow ). toContain ( 'label-position="left" label-width="130px"' )
2026-10-21 17:56:57 +08:00
expect ( workflow ). toContain ( ':confirm-loading="submitting || busy || uploadingEvidence"' )
2026-10-21 13:22:42 +08:00
expect ( workflow ). not . toContain ( 'aria-label="候选货物分页"' )
2026-10-22 23:10:09 +08:00
expect ( workflow ). toContain ( "isReview.value ? '1100px'" )
expect ( workflow ). toContain ( 'aria-label="排车与预约"' )
expect ( workflow ). toContain ( 'aria-label="审核照片"' )
2026-10-20 10:11:31 +08:00
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"' )
2026-10-21 17:22:25 +08:00
expect ( evidence ). toContain ( 'fullscreenable :loading="previewBusy"' )
expect ( evidence ). toContain ( 'class="evidence-preview" :aria-busy="previewBusy"' )
expect ( evidence ). not . toContain ( 'aria-label="本车附件预览信息"' )
expect ( evidence ). not . toContain ( '正在读取本车附件…' )
2026-10-20 10:11:31 +08:00
expect ( evidence ). toContain ( '<LoadErrorState' )
expect ( evidence ). toContain ( '<LineClampText' )
2026-10-21 14:30:33 +08:00
for ( const source of [ workflow , evidence ]) {
2026-10-20 10:11:31 +08:00
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"' )
2026-10-26 15:51:07 +08:00
expect ( source ). toContain ( 'class="density-dialog-section" aria-label="出库模式"' )
expect ( source ). toContain ( 'class="density-dialog-section" aria-label="本次出库备注"' )
2026-10-20 10:11:31 +08:00
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' ))
2026-10-20 23:34:11 +08:00
expectWorkbenchDialogs ( content )
2026-10-20 10:11:31 +08:00
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' )
2026-10-20 23:34:11 +08:00
const confirmations = confirmationClasses ( content )
expect ( confirmations ). toHaveLength ( 1 )
expect ( confirmations [ 0 ]). toEqual ( expect . arrayContaining ([ 'admin-density-confirm' , 'admin-workbench' , 'is-dialog' ]))
2026-10-20 10:11:31 +08:00
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' ))
2026-10-20 23:34:11 +08:00
expectWorkbenchDialogs ( content )
2026-10-20 10:11:31 +08:00
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' )
2026-10-20 23:34:11 +08:00
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' ]))
2026-10-20 10:11:31 +08:00
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 )
2026-10-26 15:51:07 +08:00
expect ( list ). toContain ( 'class="density-dialog-section" aria-label="预约范围核对"' )
2026-10-20 10:11:31 +08:00
expect ( list ). toContain ( 'data-tone="source"' )
2026-10-26 15:51:07 +08:00
expect ( list ). toContain ( 'class="density-dialog-section" aria-label="目的仓登记"' )
2026-10-20 10:11:31 +08:00
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:/ )
}
})
2026-10-09 11:11:20 +08:00
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"' )
})
2026-10-20 23:34:11 +08:00
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):/ )
})
2026-09-14 17:54:21 +08:00
})