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