import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' import { fileURLToPath } from 'node:url' const sidebar = readFileSync( fileURLToPath(new URL('../src/components/layout/SidebarRail.vue', import.meta.url)), 'utf8' ) const sidebarStyles = readFileSync( fileURLToPath(new URL('../src/assets/styles/components/sidebar-rail.css', import.meta.url)), 'utf8' ) const topbar = readFileSync( fileURLToPath(new URL('../src/components/layout/TopBar.vue', import.meta.url)), 'utf8' ) const topbarStyles = readFileSync( fileURLToPath(new URL('../src/assets/styles/components/top-bar.css', import.meta.url)), 'utf8' ) const appShellRouteView = readFileSync( fileURLToPath(new URL('../src/views/AppShellRouteView.vue', import.meta.url)), 'utf8' ) const documentInbox = readFileSync( fileURLToPath(new URL('../src/composables/useDocumentCenterInbox.js', import.meta.url)), 'utf8' ) const documentNewState = readFileSync( fileURLToPath(new URL('../src/utils/documentCenterNewState.js', import.meta.url)), 'utf8' ) const notificationStatesService = readFileSync( fileURLToPath(new URL('../src/services/notificationStates.js', import.meta.url)), 'utf8' ) const topbarNotificationStates = readFileSync( fileURLToPath(new URL('../src/composables/useTopBarNotificationStates.js', import.meta.url)), 'utf8' ) test('sidebar no longer renders document center unread indicators', () => { assert.doesNotMatch(sidebar, /useDocumentCenterInbox/) assert.doesNotMatch(sidebar, /hasUnread: documentInboxHasUnread/) assert.doesNotMatch(sidebar, /hasNewMessage/) assert.doesNotMatch(sidebar, /nav-label-text/) assert.doesNotMatch(sidebar, /nav-unread-dot/) assert.match(sidebar, /\{\{ item\.displayLabel \}\}<\/span>/) assert.doesNotMatch(sidebarStyles, /\.nav-label-text\s*\{/) assert.doesNotMatch(sidebarStyles, /\.nav-unread-dot/) assert.match(sidebarStyles, /\.nav-label\s*\{[\s\S]*overflow:\s*hidden;[\s\S]*text-overflow:\s*ellipsis;/) }) test('topbar bell owns document center unread notifications', () => { assert.match(topbar, /useDocumentCenterInbox/) assert.match(topbar, /useTopBarNotificationStates/) assert.match(topbar, /resolveDocumentNotificationId/) assert.match(topbar, /notificationRows: documentInboxNotificationRows/) assert.match(topbar, /const documentNotificationItems = computed/) assert.match(topbar, /title: `\$\{row\.documentTypeLabel \|\| '单据'\} \$\{row\.documentNo \|\| row\.claimId \|\| '待生成'\}`/) assert.match(topbar, /description: resolveDocumentNotificationDescription\(row\)/) assert.match(topbar, /const unread = Boolean\(row\.isUnread\) && !isNotificationRead\(id\)/) assert.match(topbar, /markDocumentInboxRowRead\(item\.documentRow\)/) assert.match(topbar, /markNotificationStateRead\(item\)/) assert.match(topbar, /markDocumentInboxRowsRead\(documentRows\)/) assert.match(topbar, /hideNotificationStates\(currentItems\)/) assert.match(topbar, /loadNotificationStates\(\)/) assert.match(topbar, /const topbarNotificationCount = computed\(\(\) => \{[\s\S]*const count = unreadNotifications\.value\.length/) assert.doesNotMatch(topbar, /document-center-unread/) assert.doesNotMatch(topbar, /target: \{ type: 'documents-center' \}/) assert.doesNotMatch(topbar, /emit\('navigate', 'documents'\)/) assert.match(appShellRouteView, /@navigate="handleNavigate"/) assert.match(topbar, /startDocumentInboxPolling\(\)/) assert.match(topbar, /stopDocumentInboxPolling\(\)/) assert.match(topbar, /class="notification-clear-btn"/) assert.match(topbar, /notificationBulkActionLabel/) assert.match(topbar, /notificationBulkActionDisabled/) assert.match(topbar, /function handleNotificationBulkAction\(\)/) assert.match(topbar, /function markUnreadNotificationsRead\(\)/) assert.match(topbar, /function deleteReadNotifications\(\)/) assert.match(topbar, /function markNotificationRead\(item\)/) assert.match(topbar, /class="notification-avatar" :class="item\.tone"/) assert.match(topbarStyles, /\.notification-head-copy\s*\{[\s\S]*display:\s*grid;/) assert.match(topbarStyles, /\.notification-head-actions\s*\{[\s\S]*display:\s*inline-flex;/) assert.match(topbarStyles, /\.notification-popover\s*\{[\s\S]*max-height:\s*min\(560px,\s*calc\(100vh - 68px\)\);/) assert.match(topbarStyles, /\.notification-list\s*\{[\s\S]*max-height:\s*min\(420px,\s*calc\(100vh - 166px\)\);[\s\S]*overflow-y:\s*auto;/) assert.match(topbarStyles, /\.notification-preview\s*\{[\s\S]*-webkit-line-clamp:\s*2;/) assert.match(topbarStyles, /@media \(max-width: 640px\)[\s\S]*\.notification-popover\s*\{[\s\S]*position:\s*fixed;/) assert.match(topbarStyles, /\.notification-tabs button em\s*\{[\s\S]*border-radius:\s*4px;/) assert.match(topbarStyles, /\.notification-row\s*\{[\s\S]*grid-template-columns:\s*52px minmax\(0,\s*1fr\);/) assert.doesNotMatch(topbarStyles, /\.notification-dot/) }) test('topbar notification bulk action label follows active tab semantics', () => { assert.match(topbar, />\s*\{\{ notificationBulkActionLabel \}\}\s*<\/button>/) assert.match(topbar, /:disabled="notificationBulkActionDisabled"/) assert.match(topbar, /@click="handleNotificationBulkAction"/) assert.match(topbar, /const notificationBulkActionLabel = computed\(\(\) => \(\s*notificationTab\.value === 'unread' \? '全部已读' : '删除已读'\s*\)\)/) assert.match(topbar, /const notificationBulkActionDisabled = computed\(\(\) => \(\s*notificationTab\.value === 'unread'\s*\? unreadNotifications\.value\.length === 0\s*: readNotifications\.value\.length === 0\s*\)\)/) assert.match(topbar, /function handleNotificationBulkAction\(\) \{[\s\S]*if \(notificationTab\.value === 'unread'\) \{[\s\S]*markUnreadNotificationsRead\(\)[\s\S]*return[\s\S]*deleteReadNotifications\(\)[\s\S]*\}/) assert.match(topbar, /function markUnreadNotificationsRead\(\) \{[\s\S]*const currentItems = unreadNotifications\.value[\s\S]*markDocumentInboxRowsRead\(documentRows\)[\s\S]*markNotificationStatesRead\(currentItems\)/) assert.match(topbar, /function deleteReadNotifications\(\) \{[\s\S]*const currentItems = readNotifications\.value[\s\S]*hideNotificationStates\(currentItems\)/) assert.doesNotMatch(topbar, />\s*清空通知\s*<\/button>/) }) test('topbar notification popover uses reference-style avatar message rows', () => { assert.match(topbar, /class="notification-avatar" :class="item\.tone"/) assert.match(topbar, /class="notification-avatar-label"/) assert.match(topbar, /class="notification-avatar-badge"/) assert.match(topbar, /class="notification-row-content"/) assert.match(topbar, /class="notification-row-top"/) assert.match(topbar, /class="notification-row-title"/) assert.match(topbar, /class="notification-preview"/) assert.match(topbar, /class="notification-time"/) assert.match(topbar, /avatarLabel: resolveDocumentNotificationAvatarLabel\(row\)/) assert.match(topbar, /avatarLabel: resolveNotificationAvatarLabel\(item\)/) assert.match(topbar, /timeLabel:\s*formatNotificationTimeLabel\(row\.updatedAt \|\| row\.createdAt\)/) assert.match(topbar, /timeLabel:\s*formatNotificationTimeLabel\(item\.time \|\| item\.updatedAt \|\| item\.due\)/) assert.doesNotMatch(topbar, /class="notification-category-pill"/) assert.doesNotMatch(topbar, /class="notification-row-action"/) assert.doesNotMatch(topbar, /