usePlanView.integration.test.js 8.67 KB
/**
 * 计划书模块集成测试
 *
 * @description 测试计划书模块的核心流程,包括查看、字段依赖、字段转换等
 * @module composables/__tests__/usePlanView.integration
 * @author Claude Code
 * @created 2026-02-14
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { ref, reactive } from 'vue'
import Taro from '@tarojs/taro'
import { viewProposal } from '../usePlanView'
import { useFieldValueTransform } from '../useFieldValueTransform'
import { useFieldDependencies } from '../useFieldDependencies'
import { PLAN_FIELD_DEFINITIONS, FIELD_GROUPS, getFieldsByGroup } from '@/config/plan-fields'
import { viewAPI } from '@/api/plan'

// Mock Taro API
vi.mock('@tarojs/taro', () => {
  const showToast = vi.fn()
  const showModal = vi.fn()
  const showLoading = vi.fn()
  const hideLoading = vi.fn()
  const showActionSheet = vi.fn()
  const navigateTo = vi.fn()
  const redirectTo = vi.fn()

  return {
    default: {
      showToast,
      showModal,
      showLoading,
      hideLoading,
      showActionSheet,
      navigateTo,
      redirectTo
    },
    // 导出命名导出以匹配 useFileOperation 中的用法
    showToast,
    showModal,
    showLoading,
    hideLoading,
    showActionSheet,
    navigateTo,
    redirectTo
  }
})

// Mock viewAPI
vi.mock('@/api/plan', () => ({
  viewAPI: vi.fn()
}))

// Mock useFileOperation
vi.mock('@/composables/useFileOperation', () => ({
  useFileOperation: () => ({
    viewFile: vi.fn().mockResolvedValue(true) // 模拟文件预览成功
  })
}))

describe('计划书模块集成测试', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  afterEach(() => {
    vi.restoreAllMocks()
  })

  describe('完整流程:查看计划书', () => {
    it('应该成功预览单文件计划书', async () => {
      viewAPI.mockResolvedValue({ code: 1 })
      const proposal = {
        id: 123,
        order_status: '7', // COMPLETED
        proposal_files: [{ file_name: '计划书.pdf', file_url: 'https://example.com/plan.pdf', id: 1 }]
      }

      await viewProposal(proposal)

      // 验证:显示预览成功提示
      expect(Taro.showToast).toHaveBeenCalledWith({
        title: '已标记为查看',
        icon: 'success'
      })

      // 验证:调用 viewAPI 标记查看
      expect(viewAPI).toHaveBeenCalledWith({ i: 123 })
    })

    it('应该显示多文件选择弹框', async () => {
      const proposal = {
        id: 456,
        order_status: '7',
        proposal_files: [
          { file_name: '计划书A.pdf', file_url: 'https://example.com/planA.pdf', id: 1 },
          { file_name: '计划书B.pdf', file_url: 'https://example.com/planB.pdf', id: 2 }
        ]
      }

      await viewProposal(proposal)

      // 验证:显示选择弹框(Taro.showActionSheet)
      expect(Taro.showActionSheet).toHaveBeenCalled()
    })

    it('应该在计划书未生成时友好提示', async () => {
      const proposal = {
        id: 789,
        order_status: '3', // PENDING
        proposal_files: []
      }

      await viewProposal(proposal)

      // 验证:显示友好提示
      expect(Taro.showToast).toHaveBeenCalledWith({
        title: '计划书尚未生成,请稍后',
        icon: 'none'
      })
    })
  })

  describe('字段依赖关系测试', () => {
    it('应该根据 withdrawal_enabled 控制字段可见性', () => {
      const formData = reactive({
        withdrawal_enabled: false
      })

      const { isFieldVisible } = useFieldDependencies(formData)

      // 当 withdrawal_enabled 为 false 时,相关字段应该不可见
      expect(isFieldVisible('withdrawal_mode')).toBe(false)
      expect(isFieldVisible('withdrawal_start_age')).toBe(false)
      expect(isFieldVisible('withdrawal_period')).toBe(false)
    })

    it('应该在启用 withdrawal_enabled 后显示相关字段', () => {
      const formData = reactive({
        withdrawal_enabled: true
      })

      const { isFieldVisible, isFieldEnabled } = useFieldDependencies(formData)

      // 当 withdrawal_enabled 为 true 时,相关字段应该可见
      expect(isFieldVisible('withdrawal_mode')).toBe(true)
      expect(isFieldVisible('withdrawal_start_age')).toBe(true)
      expect(isFieldEnabled('withdrawal_mode')).toBe(true)
    })
  })

  describe('字段转换测试', () => {
    it('应该正确转换分值为元值显示', () => {
      const formData = ref({
        coverage: 10000, // API 存的是分(整数)
        annual_premium: 10000
      })

      const { toYuan } = useFieldValueTransform(formData)

      // 分转元显示(÷100)
      expect(toYuan('coverage', 10000)).toBe('100.00')
    })

    it('应该正确转换元值为分值提交', () => {
      const formData = ref({
        coverage: '100.00', // 表单显示的是元
        annual_premium: '100.00'
      })

      const { toFen } = useFieldValueTransform(formData)

      // 元转分提交(×100)
      expect(toFen('coverage', '100.00')).toBe(10000)
    })

    it('应该批量转换表单数据为显示格式', () => {
      const formData = ref({
        coverage: 10000,
        name: '张三',
        gender: 'male'
      })

      const { displayData } = useFieldValueTransform(formData)

      expect(displayData.value.coverage).toBe('100.00')
      expect(displayData.value.name).toBe('张三')
      expect(displayData.value.gender).toBe('male')
    })

    it('应该批量转换表单数据为提交格式', () => {
      const formData = ref({
        coverage: '100.00',
        name: '张三',
        gender: 'male'
      })

      const { submitData } = useFieldValueTransform(formData)

      expect(submitData.value.coverage).toBe(10000)
      expect(submitData.value.name).toBe('张三')
      expect(submitData.value.gender).toBe('male')
    })
  })

  describe('错误处理测试', () => {
    it('应该在 proposal 参数无效时友好提示', async () => {
      const consoleSpy = vi.spyOn(console, 'error')

      await viewProposal(null)

      // 验证:记录错误日志
      expect(consoleSpy).toHaveBeenCalledWith(
        '[usePlanView] proposal 参数无效:',
        expect.any(Error)
      )

      consoleSpy.mockRestore()
    })

    it('应该在 proposal.id 缺失时友好提示', async () => {
      await viewProposal({})

      // 验证:显示友好提示
      expect(Taro.showToast).toHaveBeenCalledWith({
        title: '计划书 ID 缺失',
        icon: 'none'
      })
    })

    it('应该在 proposalFiles 为空时友好提示', async () => {
      await viewProposal({
        id: 123,
        order_status: '7',
        proposal_files: []
      })

      // 验证:显示友好提示
      expect(Taro.showToast).toHaveBeenCalledWith({
        title: '暂无可查看的计划书',
        icon: 'none'
      })
    })

    it('应该支持 onError 回调', async () => {
      const onError = vi.fn()

      await viewProposal({}, { onError })

      expect(onError).toHaveBeenCalledWith(expect.any(Error))
    })
  })

  describe('字段分组测试', () => {
    it('应该能按分组获取字段', () => {
      // 由于 getFieldsByGroup 不在 useFieldValueTransform 导出中,我们测试配置
      const basicFields = Object.values(PLAN_FIELD_DEFINITIONS).filter(f => f.group === FIELD_GROUPS.BASIC)
      const coverageFields = Object.values(PLAN_FIELD_DEFINITIONS).filter(f => f.group === FIELD_GROUPS.COVERAGE)
      const withdrawalFields = Object.values(PLAN_FIELD_DEFINITIONS).filter(f => f.group === FIELD_GROUPS.WITHDRAWAL)

      // 验证:分组正确
      expect(basicFields.length).toBeGreaterThan(0)
      expect(coverageFields.length).toBeGreaterThan(0)
      expect(withdrawalFields.length).toBeGreaterThan(0)

      // 验证:customer_name 在 BASIC 组
      expect(PLAN_FIELD_DEFINITIONS.customer_name.group).toBe(FIELD_GROUPS.BASIC)

      // 验证:coverage 在 COVERAGE 组
      expect(PLAN_FIELD_DEFINITIONS.coverage.group).toBe(FIELD_GROUPS.COVERAGE)

      // 验证:withdrawal_mode 在 WITHDRAWAL 组
      expect(PLAN_FIELD_DEFINITIONS.withdrawal_mode.group).toBe(FIELD_GROUPS.WITHDRAWAL)
    })

    it('应该通过 getFieldsByGroup 获取分组字段', () => {
      const basicFields = getFieldsByGroup(FIELD_GROUPS.BASIC)
      const coverageFields = getFieldsByGroup(FIELD_GROUPS.COVERAGE)
      const withdrawalFields = getFieldsByGroup(FIELD_GROUPS.WITHDRAWAL)

      expect(Object.keys(basicFields).length).toBeGreaterThan(0)
      expect(Object.keys(coverageFields).length).toBeGreaterThan(0)
      expect(Object.keys(withdrawalFields).length).toBeGreaterThan(0)

      expect(basicFields.customer_name).toBeDefined()
      expect(coverageFields.coverage).toBeDefined()
      expect(withdrawalFields.withdrawal_mode).toBeDefined()
    })
  })
})