324 lines
10 KiB
TypeScript
324 lines
10 KiB
TypeScript
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,非当前日期']);
|
|
});
|
|
});
|