129 lines
7.7 KiB
TypeScript
129 lines
7.7 KiB
TypeScript
import { readFileSync } from 'node:fs'
|
|
import { resolve } from 'node:path'
|
|
import ElementPlus from 'element-plus'
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
import { flushPromises, mount } from '@vue/test-utils'
|
|
import { createMemoryHistory, createRouter } from 'vue-router'
|
|
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
|
import { useAuthStore } from '@/core/auth/auth'
|
|
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
|
import { listFinanceAccountLedger, listFinanceAccounts } from '../api/finance'
|
|
import FinanceCustomerAccountsView from './FinanceCustomerAccountsView.vue'
|
|
|
|
vi.mock('../api/finance', () => ({
|
|
listFinanceAccounts: vi.fn(),
|
|
listFinanceAccountLedger: vi.fn(),
|
|
getFinanceCustomerTerms: vi.fn(),
|
|
updateFinanceCustomerTerms: vi.fn(),
|
|
}))
|
|
vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn() }))
|
|
|
|
class ResizeObserverStub implements ResizeObserver { observe() {} unobserve() {} disconnect() {} }
|
|
|
|
function memoryStorage(): Storage {
|
|
const values = new Map<string, string>()
|
|
return { get length() { return values.size }, clear: () => values.clear(), getItem: (key) => values.get(key) ?? null, key: (index) => [...values.keys()][index] ?? null, removeItem: (key) => values.delete(key), setItem: (key, value) => values.set(key, value) }
|
|
}
|
|
|
|
describe('FinanceCustomerAccountsView delivery', () => {
|
|
const page = readFileSync(resolve(process.cwd(), 'src/modules/finance/pages/FinanceCustomerAccountsView.vue'), 'utf8')
|
|
const routes = readFileSync(resolve(process.cwd(), 'src/modules/finance/routes.ts'), 'utf8')
|
|
|
|
it('keeps the account detail as a full page with traceable ledger controls', () => {
|
|
expect(page).toContain('v-else-if="!detailOpen"')
|
|
expect(page).toContain('返回客户账户')
|
|
expect(page).toContain('<DetailPageLayout')
|
|
expect(page).toContain('class="account-metrics"')
|
|
expect(page).toContain('待收金额')
|
|
expect(page).toContain('待开账金额')
|
|
expect(page).toContain('class="ledger-query"')
|
|
expect(page).toContain('class="ledger-table"')
|
|
expect(page).toContain('<template #toolbar-actions>')
|
|
expect(page).toContain('<template #pagination-left>共 {{ ledgerTotal }} 条</template>')
|
|
expect(page).not.toContain('class="detail-toolbar"')
|
|
expect(page).not.toContain('<AppDrawer')
|
|
for (const marker of ['流水类型', '发生日期', '导出流水', 'listFinanceAccountLedger']) expect(page).toContain(marker)
|
|
})
|
|
|
|
it('keeps term editing aligned with the account summary and nearby actions', () => {
|
|
for (const marker of ['class="term-section"', 'class="term-editor"', '账期天数', '#footer', '保存账期']) expect(page).toContain(marker)
|
|
expect(page).toContain('grid-template-columns:repeat(4,minmax(0,1fr))')
|
|
expect(page).toContain('grid-template-columns:minmax(180px,1fr) auto')
|
|
expect(page).toContain('class="surface-panel finance-account-detail__surface"')
|
|
expect(page).toContain('min-height:88px')
|
|
expect(page).toContain('class="detail-table-scroll"')
|
|
})
|
|
|
|
it('shows only derived CNY summaries and no wallet actions', () => {
|
|
for (const marker of ['未收', '待出账', '本期已收', '应付', 'CNY']) expect(page).toContain(marker)
|
|
for (const forbidden of ['手动充值', '钱包', '外币', '发票', '利润']) expect(page).not.toContain(forbidden)
|
|
})
|
|
|
|
it('selects an active customer by name from master data', () => {
|
|
expect(page).toContain("key: 'customerId', type: 'select'")
|
|
expect(page).toContain("label: '客户名称'")
|
|
expect(page).toContain('item.tenantName')
|
|
expect(page).not.toContain('tenantCode')
|
|
expect(page).toContain('listPlatformTenants')
|
|
expect(page).not.toContain("key: 'keyword', type: 'input'")
|
|
})
|
|
|
|
it('supports a refreshable customer detail route without inventing an API parameter', () => {
|
|
expect(routes).toContain("path: 'finance/customer-accounts/:customerId'")
|
|
expect(page).toContain("name: 'finance-customer-account-detail'")
|
|
expect(page).toContain('restoreAccountDetail(customerId)')
|
|
expect(page).toContain("listFinanceAccounts({ page: currentPage, size: 200 })")
|
|
expect(page).toContain('customerId: String(filters.value.customerId ||')
|
|
})
|
|
|
|
beforeEach(() => {
|
|
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
|
vi.stubGlobal('localStorage', memoryStorage())
|
|
vi.stubGlobal('sessionStorage', memoryStorage())
|
|
vi.mocked(listFinanceAccounts).mockReset().mockResolvedValue({ records: [{ customerId: '10001', customerCode: 'C-001', customerName: '客户A', contactName: '张三', contactPhone: '13800000000', outstandingReceivable: 100, pendingBilling: 20, receivedInPeriod: 30, outstandingPayable: 40, status: 'ACTIVE', lastChangedAt: '2026-08-13 10:00:00' }], total: 1, pages: 1 })
|
|
vi.mocked(listFinanceAccountLedger).mockReset().mockResolvedValue({ records: [{ ledgerId: '1', occurredAt: '2026-08-13T10:00:00', businessType: 'REC', documentType: 'RECEIPT', documentId: '2', documentNo: 'REC-2', receivableChange: -10, payableChange: 0, outstandingReceivableAfter: 90, operatorName: 'operator-1000001', remark: '收款' }], total: 1, pages: 1 })
|
|
vi.mocked(listPlatformTenants).mockReset().mockResolvedValue({ records: [{ id: '10001', tenantName: '客户A', status: 'ACTIVE' } as PlatformTenantResource], page: 1, size: 200, total: 1, pages: 1 })
|
|
})
|
|
|
|
it('restores account summary and ledger after opening the detail URL directly', async () => {
|
|
const pinia = createPinia(); setActivePinia(pinia)
|
|
const auth = useAuthStore(); auth.token = 'token'; auth.permissions = ['platform:finance:customer-account:list']
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
const router = createRouter({ history: createMemoryHistory(), routes: [
|
|
{ path: '/finance/customer-accounts', name: 'finance-customer-accounts', component: FinanceCustomerAccountsView },
|
|
{ path: '/finance/customer-accounts/:customerId', name: 'finance-customer-account-detail', component: FinanceCustomerAccountsView },
|
|
] })
|
|
await router.push('/finance/customer-accounts/10001'); await router.isReady()
|
|
const wrapper = mount(FinanceCustomerAccountsView, { global: { plugins: [pinia, ElementPlus, router] } })
|
|
await flushPromises()
|
|
|
|
expect(wrapper.text()).toContain('客户A')
|
|
expect(wrapper.text()).not.toContain('C-001')
|
|
expect(listFinanceAccounts).toHaveBeenCalledWith({ page: 1, size: 200 })
|
|
expect(listFinanceAccountLedger).toHaveBeenCalledWith('10001', expect.objectContaining({ page: 1, size: 20 }))
|
|
wrapper.unmount()
|
|
})
|
|
|
|
it('formats known ledger codes, operator ids and timestamps for display', async () => {
|
|
const pinia = createPinia(); setActivePinia(pinia)
|
|
const auth = useAuthStore(); auth.token = 'token'; auth.permissions = ['platform:finance:customer-account:list']
|
|
useAccessPreviewStore().initializeAfterLogin()
|
|
const router = createRouter({ history: createMemoryHistory(), routes: [
|
|
{ path: '/finance/customer-accounts', name: 'finance-customer-accounts', component: FinanceCustomerAccountsView },
|
|
{ path: '/finance/customer-accounts/:customerId', name: 'finance-customer-account-detail', component: FinanceCustomerAccountsView },
|
|
] })
|
|
await router.push('/finance/customer-accounts/10001'); await router.isReady()
|
|
const wrapper = mount(FinanceCustomerAccountsView, { global: { plugins: [pinia, ElementPlus, router] } })
|
|
await flushPromises()
|
|
expect(wrapper.text()).toContain('收款')
|
|
expect(wrapper.text()).toContain('操作人信息缺失')
|
|
expect(wrapper.text()).not.toContain('operator-1000001')
|
|
expect(wrapper.text()).not.toContain('1000001')
|
|
expect(wrapper.text()).not.toContain('2026-08-13T10:00:00')
|
|
wrapper.unmount()
|
|
})
|
|
})
|
|
|
|
afterAll(() => vi.unstubAllGlobals())
|