diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-18 09:50:37 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-18 09:50:37 +0800 |
| commit | 0b1ad75571b312dd60f9ae0099b47cc02ac54c78 (patch) | |
| tree | 1fdcdaa6085f25f6c28ea5cdb071d01c0bc50515 /frontend/src/lib/sparkline.test.ts | |
| parent | 18b2d336d058015341e93dd95fa6a8ac8e2ec67d (diff) | |
| download | strategy-lab-0b1ad75571b312dd60f9ae0099b47cc02ac54c78.tar.gz | |
feat(frontend): 首页指标卡与项目行 sparkline(前端重设计第二轮之一)
[变更性质] 新增能力,不动后端。
[新增功能] dashboardMetrics 聚合(项目/数据集/实验/AI 用量统计 + 最近回测首末净值),实盘数据优先、缺数以占位符 '-' 兜底;ProjectMetricRow 渲染 4 张 metric-card;Sparkline 组件输出 up/down 方向线段与端点圆点。
[实现方案] 聚合逻辑与展示组件分离,sparkline 仅依赖 first/last 两个字段,无图表库依赖。
[影响范围] frontend/src/lib 与 components;配套 5 组单测(dashboardMetrics/sparkline/metricRow)。
Diffstat (limited to 'frontend/src/lib/sparkline.test.ts')
| -rw-r--r-- | frontend/src/lib/sparkline.test.ts | 66 |
1 files changed, 66 insertions, 0 deletions
diff --git a/frontend/src/lib/sparkline.test.ts b/frontend/src/lib/sparkline.test.ts new file mode 100644 index 0000000..aecf20d --- /dev/null +++ b/frontend/src/lib/sparkline.test.ts @@ -0,0 +1,66 @@ +// @ts-nocheck +import { describe, expect, it, vi } from 'vitest'; +import { mount, unmount } from 'svelte'; +import Sparkline from '../components/Sparkline.svelte'; + +// Leader round2: 项目列表每行内嵌迷你权益缩略图—— +// 用最近一个回测 equity 的首尾点画 sparkline SVG; +// 无数据就整个组件留空(不渲染 svg)。 + +describe('Sparkline (real render)', () => { + it('renders an svg with a first→last polyline when handed endpoints', async () => { + const host = document.createElement('div'); + const inst = mount(Sparkline, { + target: host, + props: { first: 100, last: 90, up: false, ariaLabel: '最近回测权益缩略' } + }); + await new Promise((r) => setTimeout(r, 50)); + const svg = host.querySelector('svg'); + expect(svg, 'sparkline must be a real svg element').toBeTruthy(); + const path = svg.querySelector('path, polyline'); + expect(path).toBeTruthy(); + const d = (path.getAttribute('d') || path.getAttribute('points')) ?? ''; + // polyline from left edge to right edge: both endpoints present in geometry + expect(d).toContain('0'); // first x + const coordsCount = d.trim().split(/[\s,L,M]+/).filter(Boolean).length; + expect(coordsCount).toBeGreaterThanOrEqual(4); // 2 points × (x,y) + unmount(inst); + host.remove(); + }); + + it('renders NOTHING (no svg) for missing data — 留空不渲染', async () => { + const host = document.createElement('div'); + const inst = mount(Sparkline, { target: host, props: { first: null, last: null, ariaLabel: 'x' } }); + await new Promise((r) => setTimeout(r, 50)); + expect(host.querySelector('svg')).toBeNull(); + expect(host.textContent.trim()).toBe(''); + unmount(inst); + host.remove(); + }); + + it('方向语义:down 变红(危险色),up 变绿(成功色)', async () => { + const down = document.createElement('div'); + const inst1 = mount(Sparkline, { + target: down, + props: { first: 100, last: 90, up: false, ariaLabel: 'x' } + }); + await new Promise((r) => setTimeout(r, 30)); + const downColor = down.querySelector('path, polyline')?.getAttribute('stroke') ?? ''; + unmount(inst1); + down.remove(); + + const up = document.createElement('div'); + const inst2 = mount(Sparkline, { + target: up, + props: { first: 100, last: 130, up: true, ariaLabel: 'x' } + }); + await new Promise((r) => setTimeout(r, 30)); + const upColor = up.querySelector('path, polyline')?.getAttribute('stroke') ?? ''; + unmount(inst2); + up.remove(); + + expect(downColor).not.toBe(''); + expect(upColor).not.toBe(''); + expect(downColor).not.toBe(upColor); + }); +}); |
