diff --git a/src/app/DomesticOutboundListStates.test.ts b/src/app/DomesticOutboundListStates.test.ts new file mode 100644 index 00000000..d5d83df8 --- /dev/null +++ b/src/app/DomesticOutboundListStates.test.ts @@ -0,0 +1,39 @@ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { describe, expect, it } from 'vitest' + +const pages = [ + ['src/modules/container-loading/pages/ContainersView.vue', ':loading="loading"', 'LoadErrorState', 'empty-text="暂无货柜"', 'statusText(row.planStatus)', 'platform:firstleg:container:view'], + ['src/modules/loading-lists/components/LoadingListsView.vue', ':loading="loading"', 'LoadErrorState', ':empty-text="`暂无符合筛选条件的${title}`"', 'loadingListStatusLabel(row.status)', 'canView'], + ['src/modules/ocean-bookings/pages/OceanBookingsView.vue', ':loading="loading"', 'LoadErrorState', 'empty-text="暂无符合筛选条件的订舱"', 'oceanBookingStatusLabel(row.status)', 'platform:firstleg:ocean-booking:view'], + ['src/modules/port-schedules/pages/PortSchedulesView.vue', ':loading="loading"', 'LoadErrorState', 'empty-text="暂无符合条件的船期"', "row.expired ? '已过期' : '有效'", 'platform:firstleg:port-schedule:view'], + ['src/modules/trucking/pages/TruckAppointmentsView.vue', ':loading="loading"', 'LoadErrorState', 'empty-text="暂无符合筛选条件的拖车预约"', 'statusLabel(row.status)', 'platform:firstleg:truck-appointment:view'], + ['src/modules/customs/pages/CustomsCasesView.vue', ':loading="loading"', 'LoadErrorState', 'empty-text="暂无报关协同记录"', 'customsStatusLabel(row.status)', 'platform:firstleg:customs:view'], + ['src/modules/outbound-dispatch/pages/OutboundDispatchesView.vue', ':loading="loading"', 'LoadErrorState', 'empty-text="暂无符合条件的出仓记录"', 'outboundDispatchStatusLabel(row.dispatchStatus)', 'platform:firstleg:outbound-dispatch:view'], +] as const + +function source(path: string) { + return readFileSync(resolve(process.cwd(), path), 'utf8') +} + +describe('domestic outbound list state contracts', () => { + it.each(pages)('%s exposes loading, error, empty, translated status and view-permission states', (path, loading, error, empty, status, viewPermission) => { + const content = source(path) + expect(content).toContain(loading) + expect(content).toContain(error) + expect(content).toContain(empty) + expect(content).toContain(status) + expect(content).toContain(viewPermission) + }) + + it('keeps list component state when route details return to the cached workspace list', () => { + const layout = source('src/app/layouts/AppLayout.vue') + const oceanDetail = source('src/modules/ocean-bookings/pages/OceanBookingDetailView.vue') + const truckingDetail = source('src/modules/trucking/pages/TruckAppointmentDetailView.vue') + + expect(layout).toContain('') + expect(layout).toContain(':key="workspace.cacheKey(workspaceRouteId(renderedRoute))"') + expect(oceanDetail).toContain("router.push('/ocean-bookings')") + expect(truckingDetail).toContain("router.push({ name: 'trucking' })") + }) +}) diff --git a/src/modules/auth/pages/LoginView.test.ts b/src/modules/auth/pages/LoginView.test.ts index 67ad5c19..9e55dc8d 100644 --- a/src/modules/auth/pages/LoginView.test.ts +++ b/src/modules/auth/pages/LoginView.test.ts @@ -172,7 +172,8 @@ describe('LoginView authentication lifecycle', () => { await wrapper.get('form').trigger('submit') await flushPromises() - expect(wrapper.get('[role="alert"]').text()).toBe('network unavailable') + expect(wrapper.get('[role="alert"]').text()).toContain('登录失败,请检查账号和密码后重试') + expect(wrapper.get('[role="alert"]').text()).not.toContain('network unavailable') expect(initialize).not.toHaveBeenCalled() expect(replace).not.toHaveBeenCalled() }) diff --git a/src/modules/customers/components/CustomerBillingRulesPanel.test.ts b/src/modules/customers/components/CustomerBillingRulesPanel.test.ts index f28e4106..23981e4e 100644 --- a/src/modules/customers/components/CustomerBillingRulesPanel.test.ts +++ b/src/modules/customers/components/CustomerBillingRulesPanel.test.ts @@ -52,6 +52,20 @@ describe('CustomerBillingRulesPanel', () => { expect(document.body.textContent).toContain('标准中箱') }) + it('hides permission codes in the no-access state', async () => { + const auth = useAuthStore() + auth.permissions = [] + useAccessPreviewStore().initializeAfterLogin() + + mount(CustomerBillingRulesPanel, { attachTo: document.body, props: { customerId: '8', customerName: '柏悦物流' }, global: { plugins: [ElementPlus] } }) + await flushPromises() + + expect(document.body.textContent).toContain('暂无尺寸计算规则查看权限') + expect(document.body.textContent).toContain('请联系管理员申请尺寸计算规则查看权限') + expect(document.body.textContent).not.toContain('firstleg:dimension-config:view') + expect(getCustomerDimensionConfig).not.toHaveBeenCalled() + }) + it('submits all four rows in one update request', async () => { mount(CustomerBillingRulesPanel, { attachTo: document.body, props: { customerId: '8', customerName: '柏悦物流' }, global: { plugins: [ElementPlus] } }) await flushPromises() diff --git a/src/modules/customers/components/CustomerBillingRulesPanel.vue b/src/modules/customers/components/CustomerBillingRulesPanel.vue index 148f962a..0891fd28 100644 --- a/src/modules/customers/components/CustomerBillingRulesPanel.vue +++ b/src/modules/customers/components/CustomerBillingRulesPanel.vue @@ -183,7 +183,7 @@ onMounted(loadConfig)

按箱规尺寸计算

{{ customerName }}{{ customerCode ? `(${customerCode})` : '' }}的尺寸计算规则

-
暂无尺寸计算规则查看权限

需要权限 firstleg:dimension-config:view。

+
暂无尺寸计算规则查看权限

请联系管理员申请尺寸计算规则查看权限。

按箱规尺寸计算

编辑规则
diff --git a/src/modules/fee-items/pages/FeeItemsView.test.ts b/src/modules/fee-items/pages/FeeItemsView.test.ts index 64b1ea0a..d4ebeb3f 100644 --- a/src/modules/fee-items/pages/FeeItemsView.test.ts +++ b/src/modules/fee-items/pages/FeeItemsView.test.ts @@ -89,7 +89,7 @@ describe('FeeItemsView public master data and access', () => { expect(wrapper.find('.list-page-layout__state').exists()).toBe(true) expect(wrapper.find('.table-panel').exists()).toBe(false) expect(wrapper.text()).toContain('费用项目加载失败') - expect(wrapper.text()).toContain('Gateway unavailable') + expect(wrapper.text()).not.toContain('Gateway unavailable') }) it('renders an explicit no-access state without loading customers or fee items', async () => { diff --git a/src/modules/iam/pages/InternalUsersView.test.ts b/src/modules/iam/pages/InternalUsersView.test.ts index 3c620c84..0323f248 100644 --- a/src/modules/iam/pages/InternalUsersView.test.ts +++ b/src/modules/iam/pages/InternalUsersView.test.ts @@ -143,7 +143,8 @@ describe('InternalUsersView platform internal user management', () => { vi.mocked(listPlatformManagedUsers).mockRejectedValueOnce(new Error('Gateway unavailable')) const wrapper = await mountView() - expect(wrapper.text()).toContain('Gateway unavailable') + expect(wrapper.text()).toContain('内部用户列表加载失败,请稍后重试') + expect(wrapper.text()).not.toContain('Gateway unavailable') expect(wrapper.text()).toContain('重新加载') expect(wrapper.text()).not.toContain('zhangwei') }) diff --git a/src/modules/iam/pages/PermissionsView.test.ts b/src/modules/iam/pages/PermissionsView.test.ts index cd057853..37f35b62 100644 --- a/src/modules/iam/pages/PermissionsView.test.ts +++ b/src/modules/iam/pages/PermissionsView.test.ts @@ -97,7 +97,9 @@ describe('PermissionsView IAM mode', () => { const wrapper = mountServerView() await flushPromises() - expect(wrapper.findAll('[role="alert"]').map((item) => item.text()).join(' ')).toContain('IAM unavailable') + const alertText = wrapper.findAll('[role="alert"]').map((item) => item.text()).join(' ') + expect(alertText).toContain('权限资源加载失败,请稍后重试') + expect(alertText).not.toContain('IAM unavailable') }) it('shows only add-child directly for a built-in menu even with all permissions', async () => { diff --git a/src/modules/iam/pages/RolesView.test.ts b/src/modules/iam/pages/RolesView.test.ts index a727d0c8..97549ff8 100644 --- a/src/modules/iam/pages/RolesView.test.ts +++ b/src/modules/iam/pages/RolesView.test.ts @@ -113,6 +113,8 @@ describe('RolesView', () => { const wrapper = mountView(pinia) await flushPromises() - expect(wrapper.findAll('[role="alert"]').map((item) => item.text()).join(' ')).toContain('IAM unavailable') + const alertText = wrapper.findAll('[role="alert"]').map((item) => item.text()).join(' ') + expect(alertText).toContain('平台角色列表加载失败,请稍后重试') + expect(alertText).not.toContain('IAM unavailable') }) }) diff --git a/src/modules/inbounds/pages/InboundOrdersView.test.ts b/src/modules/inbounds/pages/InboundOrdersView.test.ts index 032ac439..347ad778 100644 --- a/src/modules/inbounds/pages/InboundOrdersView.test.ts +++ b/src/modules/inbounds/pages/InboundOrdersView.test.ts @@ -9,6 +9,7 @@ import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam' import QueryToolbar from '@/shared/components/QueryToolbar.vue' import DataTable from '@/shared/components/DataTable.vue' import { listPlatformWarehouses } from '@/modules/warehouses' +import { useAuthStore } from '@/core/auth/auth' import { downloadInboundCartonLabels, getPlatformInbound, listPlatformInbounds } from '../core/api' import type { InboundOrderDetail, InboundOrderSummary } from '../core/types' @@ -73,6 +74,7 @@ describe('InboundOrdersView', () => { beforeEach(() => { vi.clearAllMocks() setActivePinia(createPinia()) + useAuthStore().permissions = ['platform:firstleg:inbound:view'] vi.mocked(listPlatformInbounds).mockResolvedValue({ records: [summary], page: 1, size: 20, total: 1, pages: 1 }) vi.mocked(getPlatformInbound).mockResolvedValue(detail) vi.mocked(downloadInboundCartonLabels).mockResolvedValue(new Blob(['pdf'], { type: 'application/pdf' })) @@ -207,6 +209,16 @@ describe('InboundOrdersView', () => { vi.unstubAllGlobals() }) + it('does not expose row operations without the confirmed inbound view permission', async () => { + useAuthStore().permissions = [] + const wrapper = mountPage() + await flushPromises() + + expect(wrapper.find('.row-actions').exists()).toBe(false) + expect(wrapper.text()).toContain('无查看权限') + expect(wrapper.text()).not.toContain('打印箱唛') + }) + it('uses the app header as the page title and exposes compact accessible status tabs', async () => { const wrapper = mountPage() await flushPromises() @@ -317,6 +329,9 @@ describe('InboundOrdersView', () => { expect(columnSource).toContain("{ key: 'feeReviewStatus', label: '费用审核状态'") expect(source).toContain('