import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { MarketOverviewService } from './market-overview.service'; const indexSeries = { Date: ['20260817', '20260818'], Open: ['3350', '3370'], High: ['3385', '3400'], Low: ['3340', '3360'], Close: ['3360', '3380'], Amount: ['100', '120'], Volume: ['1000', '1100'], }; const indexResult = { ErrorId: '0', Value: Object.fromEntries( ['000001.SH', '399001.SZ', '399006.SZ'].map((code) => [code, indexSeries]), ), }; const breadthSnapshot = (up: string, down: string) => ({ ErrorId: '0', UpHome: up, DownHome: down, ItemNum: '0', }); const sectorResult = { ErrorId: '0', Value: [ { Code: '881319.SH', Name: '半导体' }, { Code: '881355.SH', Name: '软件服务' }, ], }; const sectorKlineResult = { ErrorId: '0', Value: { '881319.SH': { Date: ['20260817', '20260818'], Open: ['100', '105'], High: ['106', '110'], Low: ['99', '104'], Close: ['100', '108'], Amount: ['1000', '1200'], Volume: ['100', '120'], }, '881355.SH': { Date: ['20260817', '20260818'], Open: ['100', '98'], High: ['101', '99'], Low: ['97', '95'], Close: ['100', '96'], Amount: ['1000', '900'], Volume: ['100', '90'], }, }, }; const turnoverResult = { ErrorId: '0', Value: Object.fromEntries( ['000001.SH', '399001.SZ', '899050.BJ'].map((code) => [ code, { Date: ['20260817', '20260818'], Open: ['100', '100'], High: ['101', '101'], Low: ['99', '99'], Close: ['100', '100'], Amount: ['100000', '120000'], Volume: ['1000', '1100'], }, ]), ), }; describe('市场概览服务', () => { beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(new Date('2026-08-18T03:00:00.000Z')); }); afterEach(() => { vi.useRealTimers(); }); it('读取指数并返回统一快照元数据', async () => { const client = { request: vi.fn().mockResolvedValue(indexResult) }; const service = new MarketOverviewService(client); const response = await service.getIndices(); expect(response).toMatchObject({ source: 'tdx', status: 'fresh', data: { items: expect.any(Array) }, }); expect(response.snapshotId).toMatch(/^20260818-/); expect(client.request).toHaveBeenCalledWith( 'get_market_data', expect.objectContaining({ count: 5 }), ); }); it('核心指数交易日旧于上海当前日期时标记 delayed', async () => { vi.setSystemTime(new Date('2026-08-19T03:00:00.000Z')); const client = { request: vi.fn().mockResolvedValue(indexResult) }; const service = new MarketOverviewService(client); const response = await service.getIndices(); expect(response.status).toBe('delayed'); expect(response.warnings).toEqual(['核心指数交易日期为 20260818,非当前日期']); }); it('汇总三个市场指数的上涨和下跌家数并保留未知平盘数', async () => { const client = { request: vi .fn() .mockResolvedValueOnce(breadthSnapshot('10', '5')) .mockResolvedValueOnce(breadthSnapshot('20', '15')) .mockResolvedValueOnce(breadthSnapshot('3', '2')) .mockResolvedValueOnce(indexResult), }; const service = new MarketOverviewService(client); const response = await service.getBreadth(); expect(response).toMatchObject({ source: 'tdx', status: 'fresh', data: { advancing: 33, declining: 22, unchanged: null }, }); expect(client.request).toHaveBeenCalledWith( 'get_market_snapshot', expect.objectContaining({ stock_code: '000001.SH' }), ); }); it('市场宽度排除畸形快照并标记部分数据', async () => { const client = { request: vi .fn() .mockResolvedValueOnce(breadthSnapshot('10', '5')) .mockResolvedValueOnce({ ErrorId: '0', UpHome: '', DownHome: '3' }) .mockRejectedValueOnce(new Error('raw network detail')) .mockResolvedValueOnce(indexResult), }; const service = new MarketOverviewService(client); const response = await service.getBreadth(); expect(response).toMatchObject({ status: 'partial', data: { advancing: 10, declining: 5, unchanged: null }, }); expect(response.warnings[0]).toContain('深证市场、北证市场'); expect(response.warnings.join(' ')).not.toContain('raw network detail'); }); it('市场宽度使用核心指数交易日识别 delayed,不把旧快照标为 fresh', async () => { const staleIndexResult = structuredClone(indexResult); staleIndexResult.Value['000001.SH'].Date = ['20260816', '20260817']; const client = { request: vi .fn() .mockResolvedValueOnce(breadthSnapshot('10', '5')) .mockResolvedValueOnce(breadthSnapshot('20', '15')) .mockResolvedValueOnce(breadthSnapshot('3', '2')) .mockResolvedValueOnce(staleIndexResult), }; const service = new MarketOverviewService(client); const response = await service.getBreadth(); expect(response.status).toBe('delayed'); expect(response.warnings).toEqual(['市场宽度交易日期为 20260817,非当前日期']); }); it('按一级行业涨跌幅排序板块并保留 TDX 代码', async () => { const client = { request: vi.fn().mockResolvedValueOnce(sectorResult).mockResolvedValueOnce(sectorKlineResult), }; const service = new MarketOverviewService(client); const response = await service.getSectors(); expect(response).toMatchObject({ source: 'tdx', status: 'fresh', data: { items: [ expect.objectContaining({ code: '881319.SH', name: '半导体', changePercent: 8 }), expect.objectContaining({ code: '881355.SH', name: '软件服务', changePercent: -4 }), ], }, }); expect(client.request).toHaveBeenNthCalledWith(1, 'get_stock_list', { market: '16', list_type: 1, }); }); it('一级行业目录为空时返回不可用而不是 fresh 空数据', async () => { const client = { request: vi.fn().mockResolvedValue({ ErrorId: '0', Value: [] }) }; const service = new MarketOverviewService(client); const response = await service.getSectors(); expect(response).toMatchObject({ status: 'unavailable', data: null }); expect(response.warnings).toEqual(['一级行业目录为空']); expect(client.request).toHaveBeenCalledTimes(1); }); it('一级行业强弱结果最多展示前八项', async () => { const definitions = Array.from({ length: 9 }, (_, index) => ({ Code: `88100${index}.SH`, Name: `行业${index}`, })); const value = Object.fromEntries( definitions.map(({ Code }, index) => [ Code, { ...indexSeries, Close: ['100', `${101 + index}`], }, ]), ); const client = { request: vi .fn() .mockResolvedValueOnce({ ErrorId: '0', Value: definitions }) .mockResolvedValueOnce({ ErrorId: '0', Value: value }), }; const service = new MarketOverviewService(client); const response = await service.getSectors(); expect(response.status).toBe('fresh'); expect(response.data?.items).toHaveLength(8); expect(response.data?.items[0].name).toBe('行业8'); }); it('一级行业混合交易日时返回 partial,不把不同日期误标 fresh', async () => { const mixedDates = structuredClone(sectorKlineResult); mixedDates.Value['881355.SH'].Date = ['20260818', '20260819']; const client = { request: vi.fn().mockResolvedValueOnce(sectorResult).mockResolvedValueOnce(mixedDates), }; const service = new MarketOverviewService(client); const response = await service.getSectors(); expect(response).toMatchObject({ status: 'partial', data: { items: [expect.objectContaining({ code: '881355.SH', tradeDate: '20260819' })] }, }); expect(response.warnings).toEqual(['部分一级行业未返回有效行情']); }); it('一级行业交易日旧于上海当前日期时标记 delayed', async () => { vi.setSystemTime(new Date('2026-08-19T03:00:00.000Z')); const client = { request: vi.fn().mockResolvedValueOnce(sectorResult).mockResolvedValueOnce(sectorKlineResult), }; const service = new MarketOverviewService(client); const response = await service.getSectors(); expect(response.status).toBe('delayed'); expect(response.warnings).toEqual(['一级行业交易日期为 20260818,非当前日期']); }); it('按交易所指数成交额计算亿元级趋势而不宣称主力净流入', async () => { const client = { request: vi.fn().mockResolvedValue(turnoverResult) }; const service = new MarketOverviewService(client); const response = await service.getFunds(); expect(response).toMatchObject({ source: 'tdx', status: 'fresh', data: { currentAmount: 36, previousAmount: 30, changePercent: 20, unit: '亿元', marketCount: 3, tradeDate: '20260818', previousTradeDate: '20260817', }, }); expect(response.warnings).toEqual([]); }); it('成交额缺少一个市场时标记 partial 并展示实际市场数', async () => { const partialResult = structuredClone(turnoverResult); delete partialResult.Value['899050.BJ']; const client = { request: vi.fn().mockResolvedValue(partialResult) }; const service = new MarketOverviewService(client); const response = await service.getFunds(); expect(response).toMatchObject({ status: 'partial', data: { currentAmount: 24, previousAmount: 20, marketCount: 2, tradeDate: '20260818' }, }); expect(response.warnings).toEqual(['部分市场成交额不可用,当前汇总 2 个市场']); }); it('成交额交易日旧于上海当前日期时标记 delayed', async () => { vi.setSystemTime(new Date('2026-08-19T03:00:00.000Z')); const client = { request: vi.fn().mockResolvedValue(turnoverResult) }; const service = new MarketOverviewService(client); const response = await service.getFunds(); expect(response.status).toBe('delayed'); expect(response.warnings).toEqual(['成交额交易日期为 20260818,非当前日期']); }); });