stock-portfolio/__tests__/e2e-intc-bug.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

151 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}
})
})