This commit is contained in:
2026-10-20 23:34:11 +08:00
parent f155976f52
commit 13e4aa56ec
321 changed files with 6756 additions and 2110 deletions
+142 -32
View File
@@ -97,7 +97,7 @@ const notificationConsumers = [
'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']
const selfOperatedPages = ['self-operated/components/AnalyticsView.vue', 'self-operated/components/InventoryTurnoverView.vue']
const selfOperatedConsumers = [...selfOperatedPages, 'self-operated/components/SkuTrend.vue', 'self-operated/pages/OverviewView.vue']
const consumers = [
...selfOperatedConsumers,
@@ -156,14 +156,53 @@ const densityPages = [...new Set([
])]
type TemplateNode = NonNullable<NonNullable<ReturnType<typeof parse>['descriptor']['template']>['ast']>['children'][number]
type ElementNode = Extract<TemplateNode, { type: 1 }>
const classesOf = (node: ElementNode, preload = true, dense = preload, workbench = 'DOMESTIC'): string[] => node.props.flatMap(prop => {
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 []
// Evaluate only the authored density class binding with the shared component's two modes.
const value = new Function('mode', 'isPreload', 'dense', 'isDomestic', 'isOverseas', `return (${prop.exp.content})`)(preload ? 'preload' : 'confirmed', preload, dense, workbench === 'DOMESTIC', workbench === 'OVERSEAS') as Record<string, boolean>
// 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 }))),
@@ -178,6 +217,24 @@ const cssRoot = (css: string) => {
}
describe('approved Admin density ownership', () => {
it('keeps sailing cutoff controls compact and delegates editor borders to the shared opt-in', () => {
const view = parse(read('modules/sailing-calendar/pages/SailingCalendarView.vue')).descriptor
const rules = new Map<string, Record<string, string>>()
for (const style of view.styles) cssRoot(style.content).walkRules(rule => {
const values: Record<string, string> = {}
rule.walkDecls(decl => { values[decl.prop] = decl.value })
rules.set(rule.selector, values)
})
expect(rules.get('.sailing-entry-label')).toEqual({ width: '116px' })
expect(rules.get('.cutoff-presets')).toEqual({ flex: '0 0 auto' })
expect(rules.get('.sailing-date-editor')).toMatchObject({ width: '280px', 'max-width': '100%' })
expect(rules.get('.cutoff-rule-row, .cutoff-time-row')).toMatchObject({ 'flex-wrap': 'wrap' })
expect(view.template!.content).toContain('density-table-editor density-table-editor--outlined cutoff-days-editor')
expect(view.template!.content).toContain('density-table-editor--outlined cutoff-time-editor')
expect(view.template!.content).toContain('role="status" aria-live="polite"')
expect(view.styles.map(style => style.content).join('\n')).not.toContain('--el-input-border-color')
})
it('outlines only the three opted-in customer billing editors without overriding native focus or disabled styles', () => {
const rules = new Map<string, Record<string, string>>()
cssRoot(shared).walkRules(rule => {
@@ -362,7 +419,8 @@ describe('approved Admin density ownership', () => {
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')
expect(dialog).toContain('class="admin-density-dialog density-confirm-dialog"')
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; }')
@@ -399,7 +457,8 @@ describe('approved Admin density ownership', () => {
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="110px"')
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')
@@ -421,7 +480,8 @@ describe('approved Admin density ownership', () => {
expect(page.template!.content).toMatch(/class="[^"]*density-status-tabs[^"]*" role="tablist"/)
expect(page.styles.map((style) => style.content).join('\n')).not.toMatch(/\.(status-tabs?|supplier-tabs|container-scope-tabs)\s*[{:.]/)
}
expect(read('modules/overseas-warehouse/pages/ReturnInventoryManagementView.vue')).toContain('class="density-element-tabs density-status-tabs"')
const 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')
@@ -436,15 +496,21 @@ describe('approved Admin density ownership', () => {
it('keeps dashboard visualizations outside detail-field table styling', () => {
const dashboard = read('modules/dashboard/pages/DashboardView.vue')
expect(dashboard).toContain('class="dashboard-metrics"')
expect(dashboard).toContain('class="dashboard-metric-mark"')
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 dashboard-page">')
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')
@@ -521,7 +587,8 @@ describe('approved Admin density ownership', () => {
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')
expect(source).toContain('class="warehouse-import-dialog admin-density-dialog"')
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')
@@ -591,7 +658,7 @@ describe('approved Admin density ownership', () => {
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>'))
expect(dialog).toContain('class="admin-density-dialog"')
expectWorkbenchDialogs(source)
expect(dialog).toContain('fullscreenable')
expect(dialog).toContain('data-tone="summary"')
expect(dialog).toContain('class="task-detail-grid density-fields"')
@@ -606,7 +673,7 @@ describe('approved Admin density ownership', () => {
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>'))
expect(dialog).toContain('class="admin-density-dialog"')
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"')
@@ -621,7 +688,9 @@ describe('approved Admin density ownership', () => {
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]) {
expect(source).toContain('class="admin-density-dialog" fullscreenable :confirm-loading="saving"')
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"')
@@ -632,19 +701,20 @@ describe('approved Admin density ownership', () => {
}
expect(editor).toContain('v-if="readonly"')
expect(editor).toContain('class="density-fields fleet-quote-facts"')
expect(read('modules/overseas-master-data/pages/FleetQuotesView.vue')).toContain("customClass: 'admin-density-confirm'")
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 = parse(source).descriptor.template!.content.match(/<AppDialog[\s\S]*?<\/AppDialog>/g)!
const dialogs = expectWorkbenchDialogs(source)
expect(dialogs.length).toBeGreaterThan(0)
for (const dialog of dialogs) {
expect(dialog).toContain('class="admin-density-dialog"')
expect(dialog).not.toContain('label-position="top"')
expect(dialog).not.toContain('#header')
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')
@@ -660,10 +730,13 @@ describe('approved Admin density ownership', () => {
it('keeps the supplier license dialog styled at its own teleported root', () => {
const source = read('modules/suppliers/components/SupplierLicenseDialog.vue')
expect(source).toContain('<AppDialog class="admin-density-dialog"')
expectWorkbenchDialogs(source)
expect(source).toContain('width="min(560px, calc(100vw - 24px))"')
expect(source).toContain('aria-label="营业执照信息"')
expect(source).toContain('class="density-dialog-section" data-tone="source"')
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')
@@ -731,10 +804,16 @@ describe('approved Admin density ownership', () => {
expect(source).toContain('dense?: boolean')
const ast = parse(source).descriptor.template!.ast!
const root = ast.children.find(node => node.type === 1) as ElementNode
expect(classesOf(root, false, true).some(name => name.startsWith('admin-density')), file).toBe(true)
expect(classesOf(root, false, false).some(name => name.startsWith('admin-density')), file).toBe(false)
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')
@@ -761,8 +840,14 @@ describe('approved Admin density ownership', () => {
for (const file of ['loading-lists/components/LoadingListDetailDrawer.vue', 'port-schedules/components/CarrierScheduleQueryPanel.vue']) {
const ast = parse(read(`modules/${file}`)).descriptor.template!.ast!
const root = ast.children.find(node => node.type === 1) as ElementNode
expect(classesOf(root, false, true)).toContain('admin-density')
expect(classesOf(root, false, false)).not.toContain('admin-density')
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')
@@ -883,11 +968,12 @@ describe('approved Admin density ownership', () => {
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"')
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('background: var(--el-bg-color)')
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', () => {
@@ -940,6 +1026,8 @@ describe('approved Admin density ownership', () => {
})
expect(rules.get('.admin-density .el-table__body tr.el-table__row--striped td.el-table__cell')?.background).toBe('var(--admin-row-stripe)')
expect(rules.get('.admin-density-dialog .el-descriptions__label.el-descriptions__cell')?.background).toBe('var(--admin-label-bg)')
expect(rules.get('.admin-density-dialog .el-form--inline .el-form-item__label')?.['align-items']).toBe('center')
expect(rules.get('.admin-density-dialog.el-dialog.admin-density-workspace > .el-dialog__body')?.flex).toBe('1 1 auto')
for (const tone of ['warning', 'purple', 'primary', 'success', 'danger']) {
expect(rules.get(`:is(.admin-density, .admin-density-dialog) .density-action[data-tone="${tone}"]`)?.['--action-hover-bg']).toBe(`var(--admin-action-${tone}-hover)`)
}
@@ -1055,6 +1143,7 @@ describe('approved Admin density ownership', () => {
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("dense ? 'min(1000px, calc(100vw - 28px))' : 'min(560px, calc(100vw - 28px))'")
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) => {
@@ -1123,7 +1212,12 @@ describe('approved Admin density ownership', () => {
expect(source).toContain('density-table density-table--editable dispatch-create-table')
expect(source).toContain('<DispatchResourceFields continuous')
expect(source).toContain('label-position="left" label-width="130px"')
for (const tone of ['summary', 'source', 'work']) expect(source).toContain(`data-tone="${tone}"`)
expect(source).toContain('class="density-dialog-section dispatch-setup"')
expect(source).toContain('class="density-dialog-section dispatch-draft-toolbar"')
expect(source).toContain('class="density-form dispatch-setup-fields"')
expect(source).not.toContain('data-tone="work"')
expect(source).not.toContain('data-tone="summary"')
expect(source).not.toContain('data-tone="source"')
expect(source).toContain('class="cargo-workspace"')
expect(source).toContain('<LineClampText')
expect(source).toContain('class="dispatch-source-group"')
@@ -1233,7 +1327,7 @@ describe('approved Admin density ownership', () => {
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'))
expect(dialog).toContain('class="admin-density-dialog"')
expectWorkbenchDialogs(content)
expect(dialog).toContain('fullscreenable')
expect(dialog).toContain('920px')
expect(dialog).toContain('data-tone="summary"')
@@ -1242,7 +1336,9 @@ describe('approved Admin density ownership', () => {
expect(dialog).toContain('preview-teleported')
expect(dialog).toContain('<LineClampText')
expect(dialog).not.toContain('<el-timeline')
expect(content).toContain("customClass: 'admin-density-confirm'")
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:/)
@@ -1250,14 +1346,16 @@ describe('approved Admin density ownership', () => {
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'))
expect(dialog).toContain('class="admin-density-dialog"')
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')
expect(content.match(/customClass: 'admin-density-confirm'/g)).toHaveLength(2)
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', () => {
@@ -1426,4 +1524,16 @@ describe('approved Admin density ownership', () => {
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):/)
})
})