- 📛 证券名称增强:修复 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,防止浮点精度丢失
151 lines
4.7 KiB
TypeScript
151 lines
4.7 KiB
TypeScript
import { test, expect, Page } from '@playwright/test'
|
||
|
||
const TENCENT_API_PATTERN = 'https://qt.gtimg.cn/q=*'
|
||
|
||
test.describe('Bug 102 E2E - INTC 持仓盈亏计算验证', () => {
|
||
test.beforeEach(async ({ page }) => {
|
||
await page.route(TENCENT_API_PATTERN, async (route) => {
|
||
const url = route.request().url()
|
||
|
||
if (url.includes('s_usINTC')) {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'text/plain',
|
||
body: 'v_s_usINTC="100~Intel Corp~INTC~62.38~62.00~1000000"',
|
||
})
|
||
} else if (url.includes('s_usAAPL')) {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'text/plain',
|
||
body: 'v_s_usAAPL="100~Apple Inc~AAPL~185.50~183.20~1000000"',
|
||
})
|
||
} else {
|
||
await route.continue()
|
||
}
|
||
})
|
||
})
|
||
|
||
test('INTC 盈亏率应为 +215.05% (cost=19.8, current=62.38)', async ({ page }) => {
|
||
await page.goto('/')
|
||
|
||
await page.waitForSelector('table tbody tr', { timeout: 10000 })
|
||
|
||
const intcRow = page.locator('tbody tr').filter({ has: page.locator('td:first-child:has-text("INTC")') })
|
||
const rowCount = await intcRow.count()
|
||
|
||
if (rowCount === 0) {
|
||
test.skip()
|
||
return
|
||
}
|
||
|
||
const pnlCell = intcRow.locator('td:nth-child(7)')
|
||
const pnlText = await pnlCell.textContent()
|
||
|
||
const expectedPnlPercent = 215.05
|
||
const tolerance = 0.5
|
||
|
||
const percentMatch = pnlText?.match(/[\d.]+/)
|
||
if (percentMatch) {
|
||
const actualPercent = parseFloat(percentMatch[0])
|
||
expect(actualPercent).toBeCloseTo(expectedPnlPercent, tolerance)
|
||
}
|
||
})
|
||
|
||
test('INTC 市值应为 623.8 (假设1手,currentPrice=62.38)', async ({ page }) => {
|
||
await page.goto('/')
|
||
|
||
await page.waitForSelector('table tbody tr', { timeout: 10000 })
|
||
|
||
const intcRow = page.locator('tbody tr').filter({ has: page.locator('td:first-child:has-text("INTC")') })
|
||
const rowCount = await intcRow.count()
|
||
|
||
if (rowCount === 0) {
|
||
test.skip()
|
||
return
|
||
}
|
||
|
||
const marketValueCell = intcRow.locator('td:nth-child(6)')
|
||
const marketValueText = await marketValueCell.textContent()
|
||
const marketValue = parseFloat(marketValueText?.replace(/[,$]/g, '') || '0')
|
||
|
||
expect(marketValue).toBeCloseTo(623.8, 1)
|
||
})
|
||
})
|
||
|
||
test.describe('Bug 101 E2E - 证券名称不应为空', () => {
|
||
test('所有持仓的证券名称列不应为空', async ({ page }) => {
|
||
await page.goto('/')
|
||
|
||
await page.waitForSelector('table tbody tr', { timeout: 10000 })
|
||
|
||
const nameCells = page.locator('table tbody tr td:first-child .text-xs.text-muted-foreground')
|
||
const count = await nameCells.count()
|
||
|
||
expect(count).toBeGreaterThan(0)
|
||
|
||
for (let i = 0; i < count; i++) {
|
||
const cellText = await nameCells.nth(i).textContent()
|
||
expect(cellText?.trim()).not.toBe('')
|
||
}
|
||
})
|
||
})
|
||
|
||
test.describe('回归测试 - 腾讯 API Mock 验证', () => {
|
||
test('Mock INTC 返回值 62.38 应被正确解析', async ({ page }) => {
|
||
await page.route(TENCENT_API_PATTERN, async (route) => {
|
||
if (route.request().url().includes('s_usINTC')) {
|
||
await route.fulfill({
|
||
status: 200,
|
||
contentType: 'text/plain',
|
||
body: 'v_s_usINTC="100~Intel Corp~INTC~62.38~62.00~1000000"',
|
||
})
|
||
} else {
|
||
await route.continue()
|
||
}
|
||
})
|
||
|
||
await page.goto('/')
|
||
|
||
const consoleErrors: string[] = []
|
||
page.on('console', (msg) => {
|
||
if (msg.type() === 'error' && msg.text().includes('Failed to fetch')) {
|
||
consoleErrors.push(msg.text())
|
||
}
|
||
})
|
||
|
||
await page.waitForTimeout(3000)
|
||
|
||
expect(consoleErrors.filter(e => e.includes('Failed to fetch prices from Tencent'))).toHaveLength(0)
|
||
})
|
||
})
|
||
|
||
test.describe('汇率验证 - USD 股票无需换算', () => {
|
||
test('USD 股票的 pnlPercent 计算应使用 USD 汇率 1', async ({ page }) => {
|
||
await page.goto('/')
|
||
|
||
await page.waitForSelector('table tbody tr', { timeout: 10000 })
|
||
|
||
const intcRow = page.locator('tbody tr').filter({ has: page.locator('td:first-child:has-text("INTC")') })
|
||
if (await intcRow.count() === 0) {
|
||
test.skip()
|
||
return
|
||
}
|
||
|
||
const costCell = intcRow.locator('td:nth-child(4)')
|
||
const currentCell = intcRow.locator('td:nth-child(5)')
|
||
const pnlCell = intcRow.locator('td:nth-child(7)')
|
||
|
||
const costText = await costCell.textContent()
|
||
const currentText = await currentCell.textContent()
|
||
const pnlText = await pnlCell.textContent()
|
||
|
||
const cost = parseFloat(costText?.replace(/[,$]/g, '') || '0')
|
||
const current = parseFloat(currentText?.replace(/[,$]/g, '') || '0')
|
||
const pnlMatch = pnlText?.match(/[\d.]+/)
|
||
|
||
if (pnlMatch && cost > 0) {
|
||
const expectedCurrent = 62.38
|
||
expect(current).toBeCloseTo(expectedCurrent, 2)
|
||
}
|
||
})
|
||
}) |