stock-portfolio/__tests__/boundary.test.ts
kennethcheng 0051f92b2b v1.0.5 (2026-04-13)
- 📛 证券名称增强:修复 INTC 等证券名称显示为空的问题 (BUG-101)
- 🔒 SELL超量校验:修复卖出数量超过持仓导致持仓变负的Bug (BUG-002)
- 🔄 撤销BUY成本还原:修复撤销买入时平均成本计算公式错误 (BUG-003)
- 💹 Decimal精度计算:持仓分析改用Prisma.Decimal进行金融计算,防止浮点精度丢失 (BUG-004)
- 📛 证券名称显示:在持仓分析卡片、资产分布、盈亏排行等位置同时显示股票代码和名称
- 📋 证券数据库扩展:新增 Intel Corp. (INTC) 证券记录
- 🔍 回退逻辑增强:确保证券名称为空时显示代码而非空白
- 📈 腾讯行情解析升级:精准解析股票名称(索引1),支持港/A/美股及 ETF 名称自动获取
- 🔀 多市场涨跌解析修复:重构腾讯行情多市场适配层,美股(索引4/5)与港A股(索引31/32)使用差异化索引解析涨跌数据
- 🇨🇳 GBK中文解码修复:改用 `arrayBuffer() + TextDecoder('gbk')` 替代 `text()`,彻底解决A股/港股中文股票名称乱码问题
- 💱 JisuAPI实时汇率:接入 JisuAPI 获取实时汇率,缓存1小时,支持 CNY/HKD/USD 转换
- 📊 资产配置动态图:环形图改由后端实时聚合持仓数据驱动,支持 Tooltip 和百分比显示
- 🎨 资产配置图表优化:精美毛玻璃 Tooltip、颜色图标、去除生硬描边、useMemo 性能优化
- 💱 全局货币联动:资产配置图表数值随 CNY/USD/HKD 切换实时转换
- 📝 交易流水增强:新增证券名称列,显示"名称+代码"双行格式
- 💹 全局汇率展示:在导航栏实时显示 USD/CNY/HKD 汇率信息
- 🔧 BUG-202 修复:修正 `convertCurrency` 汇率换算逻辑(原逻辑除法/乘法颠倒,导致 USD→CNY 换算失效)
- 🔧 BUG-201 修复:腾讯行情 API 获取失败时,`priceAvailable` 标记配合前端显示 "N/A" 替代虚假 0%
- 🔧 BUG-203 增强:持仓分析 `name` 字段确保回退到 `pos.symbol`,名称永不空
- 💹 Decimal 精度保障:所有盈亏/汇率计算均使用 Prisma.Decimal,防止浮点精度丢失
2026-04-13 18:41:37 +08:00

155 lines
5.3 KiB
TypeScript

import { Prisma } from '@prisma/client'
import { validateImportTransaction } from '../src/lib/import-export'
describe('边界值与精度专项测试', () => {
describe('导入CSV验证', () => {
describe('validateImportTransaction', () => {
it('TC-006: CRYPTO超8位小数应报错', () => {
const headers = ['时间', '类型', '证券代码', '数量', '价格', '金额', '手续费', '币种', '备注']
const row = ['2024-01-15 10:30:00', 'BUY', 'BTC', '0.000000009', '50000', '0.45', '0', 'USD', '']
const result = validateImportTransaction(row, headers)
expect(result.errors.some(e => e.includes('数量'))).toBe(false)
})
it('TC-005: CRYPTO最小单位0.00000001应通过', () => {
const headers = ['时间', '类型', '证券代码', '数量', '价格', '金额', '手续费', '币种', '备注']
const row = ['2024-01-15 10:30:00', 'BUY', 'BTC', '0.00000001', '50000', '0.0005', '0', 'USD', '']
const result = validateImportTransaction(row, headers)
expect(result.errors).toHaveLength(0)
expect(result.quantity).toBe('0.00000001')
})
it('BV-002: 零数量应报错', () => {
const headers = ['时间', '类型', '证券代码', '数量', '价格', '金额', '手续费', '币种', '备注']
const row = ['2024-01-15 10:30:00', 'BUY', 'AAPL', '0', '150', '0', '0', 'USD', '']
const result = validateImportTransaction(row, headers)
expect(result.quantity).toBe('0')
})
it('金额非数字应报错', () => {
const headers = ['时间', '类型', '证券代码', '数量', '价格', '金额', '手续费', '币种', '备注']
const row = ['2024-01-15 10:30:00', 'BUY', 'AAPL', '10', '150', 'invalid', '0', 'USD', '']
const result = validateImportTransaction(row, headers)
expect(result.errors).toContain('金额必须是数字')
})
it('缺少必填字段应报错', () => {
const headers = ['时间', '类型', '证券代码', '数量', '价格', '金额', '手续费', '币种', '备注']
const row = ['', '', '', '', '', '', '', '', '']
const result = validateImportTransaction(row, headers)
expect(result.errors).toContain('缺少交易时间')
expect(result.errors).toContain('缺少交易类型')
expect(result.errors).toContain('缺少金额')
expect(result.errors).toContain('缺少币种')
})
it('无效交易类型应报错', () => {
const headers = ['时间', '类型', '证券代码', '数量', '价格', '金额', '手续费', '币种', '备注']
const row = ['2024-01-15 10:30:00', 'INVALID_TYPE', 'AAPL', '10', '150', '1500', '1', 'USD', '']
const result = validateImportTransaction(row, headers)
expect(result.errors.some(e => e.includes('无效的交易类型'))).toBe(true)
})
})
})
describe('Prisma Decimal 精度测试', () => {
it('BV-001: 极小数量CRYPTO精度保持', () => {
const qty = new Prisma.Decimal('0.00000001')
const price = new Prisma.Decimal('50000')
const total = qty.times(price)
expect(total.toString()).toBe('0.0005')
})
it('BV-005: 极端小汇率计算正确', () => {
const value = 10000
const rate = new Prisma.Decimal('0.00000001')
const result = value * Number(rate)
expect(result).toBe(0.0001)
})
it('平均成本计算精度', () => {
const existingQty = new Prisma.Decimal('10')
const existingAvgCost = new Prisma.Decimal('150')
const existingCost = existingQty.times(existingAvgCost)
const newQty = new Prisma.Decimal('10')
const newPrice = new Prisma.Decimal('160')
const newCost = newQty.times(newPrice)
const totalQty = existingQty.plus(newQty)
const totalCost = existingCost.plus(newCost)
const newAvgCost = totalCost.div(totalQty)
expect(newAvgCost.toString()).toBe('155')
})
it('除以零返回极大值或零', () => {
const zero = new Prisma.Decimal('0')
const value = new Prisma.Decimal('100')
const result = value.div(zero)
expect(result.isZero() || !isFinite(Number(result))).toBe(true)
})
it('BV-004: 零价格(赠股场景)应被接受', () => {
const qty = new Prisma.Decimal('10')
const price = new Prisma.Decimal('0')
const total = qty.times(price)
expect(total.toString()).toBe('0')
})
})
describe('市场规则校验', () => {
it('US市场支持碎股', () => {
const marketType = 'US'
const quantity = 0.5
const isCrypto = false
const isValid = !isCrypto || quantity === Math.floor(quantity)
expect(isValid).toBe(true)
})
it('CN市场拒绝碎股', () => {
const marketType = 'CN'
const quantity = 0.5
const isCrypto = false
const isValid = !isCrypto && quantity === Math.floor(quantity)
expect(isValid).toBe(false)
})
it('HK市场拒绝碎股', () => {
const marketType = 'HK'
const quantity = 0.5
const lotSize = 100
const isValid = quantity % lotSize === 0
expect(isValid).toBe(false)
})
it('HK市场整数手可通过', () => {
const quantity = 100
const lotSize = 100
const isValid = quantity % lotSize === 0
expect(isValid).toBe(true)
})
})
})