summaryrefslogtreecommitdiff
path: root/frontend/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/lib')
-rw-r--r--frontend/src/lib/datasetCardRetry.test.ts113
-rw-r--r--frontend/src/lib/datasetCardWarnings.test.ts91
-rw-r--r--frontend/src/lib/projectDataTabRecreated.test.ts19
-rw-r--r--frontend/src/lib/resultsAccounting.test.ts2
4 files changed, 224 insertions, 1 deletions
diff --git a/frontend/src/lib/datasetCardRetry.test.ts b/frontend/src/lib/datasetCardRetry.test.ts
new file mode 100644
index 0000000..df9dc31
--- /dev/null
+++ b/frontend/src/lib/datasetCardRetry.test.ts
@@ -0,0 +1,113 @@
+import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
+import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/svelte';
+
+vi.mock('../lib/client', () => {
+ const real = vi.importActual<typeof import('../lib/client')>('../lib/client');
+ return {
+ ...real,
+ getDataset: vi.fn(),
+ getDatasetPreview: vi.fn(),
+ createDataset: vi.fn()
+ };
+});
+
+import DatasetCard from '../components/DatasetCard.svelte';
+import { getDataset, createDataset } from '../lib/client';
+import type { Dataset } from '../lib/types';
+
+const failedDs: Dataset = {
+ id: 'ds-failed-1',
+ name: '国泰自由现金流-20251231-20260917',
+ request: {
+ name: '国泰自由现金流-20251231-20260917',
+ instruments: [
+ { symbol: '159399', market: 'SZ', asset_type: 'etf', name: '国泰自由现金流', currency: null }
+ ],
+ start_date: '2025-12-31',
+ end_date: '2026-09-17',
+ frequency: 'daily',
+ adjustment: 'none',
+ fields: ['open', 'high', 'low', 'close', 'volume']
+ },
+ status: 'failed',
+ error: '159399: eastmoney failed (RemoteDisconnected); tencent fallback failed (tencent source has no listed-ETF daily adapter)',
+ created_at: '2026-09-17T01:00:00Z'
+};
+
+beforeEach(() => {
+ vi.mocked(getDataset).mockResolvedValue(failedDs);
+ vi.mocked(createDataset).mockReset();
+});
+
+afterEach(() => cleanup());
+
+describe('failed dataset retry affordance (159399 regression: user could never re-request)', () => {
+ it('failed dataset offers recreate-with-same-request button and original error stays visible', async () => {
+ render(DatasetCard, { props: { dsId: 'ds-failed-1' } });
+ await screen.findByText(/eastmoney failed/);
+ const btn = await screen.findByRole('button', { name: /使用相同请求重新获取/ });
+ expect(btn).toBeTruthy();
+ });
+
+ it('recreate preserves ds.name (canonical request has no name) via the normal create API and keeps the failed record untouched', async () => {
+ vi.mocked(createDataset).mockResolvedValue({
+ ...failedDs,
+ id: 'ds-new-2',
+ status: 'pending',
+ error: null
+ });
+ render(DatasetCard, { props: { dsId: 'ds-failed-1' } });
+ const btn = await screen.findByRole('button', { name: /使用相同请求重新获取/ });
+ await fireEvent.click(btn);
+ await waitFor(() => {
+ expect(createDataset).toHaveBeenCalledTimes(1);
+ });
+ expect(vi.mocked(createDataset).mock.calls[0][0]).toStrictEqual({
+ ...failedDs.request,
+ name: failedDs.name,
+ instruments: [{ ...failedDs.request.instruments[0], currency: null }]
+ });
+ // new dataset becomes visible/trackable: hint points to the list below
+ await screen.findByText(/新数据集出现在下方列表并自动跟踪进度/);
+ });
+
+ it('recreate button is disabled after a successful recreate (no duplicate submissions), error clears button state instead', async () => {
+ vi.mocked(createDataset).mockResolvedValue({
+ ...failedDs,
+ id: 'ds-new-3',
+ status: 'pending',
+ error: null
+ });
+ render(DatasetCard, { props: { dsId: 'ds-failed-1' } });
+ const btn = await screen.findByRole('button', { name: /使用相同请求重新获取/ });
+ expect((btn as HTMLButtonElement).disabled).toBe(false);
+ await fireEvent.click(btn);
+ await screen.findByText(/已用原请求创建新数据集/);
+ const done = screen.getByRole('button', { name: /使用相同请求重新获取/ });
+ expect((done as HTMLButtonElement).disabled).toBe(true);
+ });
+
+ it('onRecreated callback fires with the new dataset so parents can refresh', async () => {
+ vi.mocked(createDataset).mockResolvedValue({
+ ...failedDs,
+ id: 'ds-new-4',
+ status: 'pending',
+ error: null
+ });
+ const onRecreated = vi.fn();
+ render(DatasetCard, { props: { dsId: 'ds-failed-1', onRecreated } });
+ const btn = await screen.findByRole('button', { name: /使用相同请求重新获取/ });
+ await fireEvent.click(btn);
+ await screen.findByText(/已用原请求创建新数据集/);
+ expect(onRecreated).toHaveBeenCalledTimes(1);
+ expect(onRecreated.mock.calls[0][0].id).toBe('ds-new-4');
+ });
+
+ it('recreate failure surfaces the error message instead of a silent no-op', async () => {
+ vi.mocked(createDataset).mockRejectedValue(new Error('HTTP400: validation'));
+ render(DatasetCard, { props: { dsId: 'ds-failed-1' } });
+ const btn = await screen.findByRole('button', { name: /使用相同请求重新获取/ });
+ await fireEvent.click(btn);
+ await screen.findByText(/重新获取失败/);
+ });
+});
diff --git a/frontend/src/lib/datasetCardWarnings.test.ts b/frontend/src/lib/datasetCardWarnings.test.ts
new file mode 100644
index 0000000..8e79e84
--- /dev/null
+++ b/frontend/src/lib/datasetCardWarnings.test.ts
@@ -0,0 +1,91 @@
+import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
+import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/svelte';
+
+vi.mock('../lib/client', () => {
+ const real = vi.importActual<typeof import('../lib/client')>('../lib/client');
+ return {
+ ...real,
+ getDataset: vi.fn(),
+ getDatasetPreview: vi.fn(),
+ createDataset: vi.fn()
+ };
+});
+
+import DatasetCard from '../components/DatasetCard.svelte';
+import { getDataset, getDatasetPreview } from '../lib/client';
+import type { Dataset, DatasetPreview } from '../lib/types';
+
+const readyDs: Dataset = {
+ id: 'ds-ready-1',
+ name: '国泰自由现金流-20251231-20260917',
+ request: {
+ instruments: [
+ { symbol: '159399', market: 'SZ', asset_type: 'etf', name: '国泰自由现金流ETF', currency: null }
+ ],
+ start_date: '2025-12-31',
+ end_date: '2026-09-17',
+ frequency: 'daily',
+ adjustment: 'none',
+ fields: ['open', 'high', 'low', 'close', 'volume']
+ },
+ status: 'ready',
+ created_at: '2026-09-17T01:00:00Z'
+};
+
+const preview: DatasetPreview = {
+ columns: ['date', 'open', 'high', 'low', 'close', 'volume'],
+ rows: [
+ { date: '2026-09-16', open: 1.001, high: 1.001, low: 0.982, close: 0.992, volume: 144526400 }
+ ],
+ coverage: [
+ {
+ instrument: '159399',
+ market: 'SZ',
+ asset_type: 'etf',
+ requested_start: '2025-12-31',
+ requested_end: '2026-09-17',
+ actual_start: '2025-12-31',
+ actual_end: '2026-09-16',
+ row_count: 173,
+ warnings: [
+ 'sina volume unit is 股 (shares)',
+ 'provider_fallback: eastmoney attempt failed; served by sina',
+ 'actual coverage 2025-12-31..2026-09-16 differs from request; gaps kept'
+ ]
+ }
+ ],
+ warnings: []
+};
+
+beforeEach(() => {
+ vi.mocked(getDataset).mockResolvedValue(readyDs);
+ vi.mocked(getDatasetPreview).mockReset();
+});
+
+afterEach(() => cleanup());
+
+describe('per-instrument provenance warnings must be visible even without the range warning banner (159399 units/provider_fallback disclosure)', () => {
+ it('renders 夲-instrument sina volume-unit and provider_fallback warnings even when the common range banner shows 覆盖平稳', async () => {
+ vi.mocked(getDatasetPreview).mockResolvedValue(preview);
+ render(DatasetCard, { props: { dsId: 'ds-ready-1' } });
+ const btn = await screen.findByRole('button', { name: /查看数据覆盖预览/ });
+ await fireEvent.click(btn);
+ await waitFor(() => {
+ expect(screen.getByText(/provider_fallback/)).toBeTruthy();
+ expect(screen.getByText(/volume unit is 股/)).toBeTruthy();
+ expect(screen.getByText(/differs from request/)).toBeTruthy();
+ });
+ });
+
+ it('no warnings: shows the plain 覆盖平稳 banner and no warning list', async () => {
+ vi.mocked(getDatasetPreview).mockResolvedValue({
+ ...preview,
+ coverage: [{ ...preview.coverage[0], warnings: [] }]
+ });
+ render(DatasetCard, { props: { dsId: 'ds-ready-1' } });
+ const btn = await screen.findByRole('button', { name: /查看数据覆盖预览/ });
+ await fireEvent.click(btn);
+ await screen.findByText(/覆盖平稳/);
+ expect(screen.queryByText(/provider_fallback/)).toBeNull();
+ });
+});
diff --git a/frontend/src/lib/projectDataTabRecreated.test.ts b/frontend/src/lib/projectDataTabRecreated.test.ts
new file mode 100644
index 0000000..1a922c2
--- /dev/null
+++ b/frontend/src/lib/projectDataTabRecreated.test.ts
@@ -0,0 +1,19 @@
+import { describe, expect, it } from 'vitest';
+// leader review regression (2026-09-17): the recreated dataset previously
+// stayed invisible — the parent (ProjectDataTab) never reloaded, the user
+// remained on the failed card. The wiring must exist AND the callback must be
+// the parent's own load (so pending→ready/failed tracking resumes).
+import dataTabSource from '../pages/ProjectDataTab.svelte?raw';
+import wizardSource from '../components/DatasetWizard.svelte?raw';
+
+describe('ProjectDataTab reloads when a failed dataset is re-requested', () => {
+ it('DatasetCard receives onRecreated bound to the parent reload', () => {
+ const src: string = dataTabSource as unknown as string;
+ expect(src).toMatch(/onRecreated=\{\(\) => void load\(\)\}/);
+ });
+
+ it('dataset card posts the create through the same client API the wizard uses', () => {
+ const src: string = wizardSource as unknown as string;
+ expect(src).toMatch(/createDataset/);
+ });
+});
diff --git a/frontend/src/lib/resultsAccounting.test.ts b/frontend/src/lib/resultsAccounting.test.ts
index 226e1d0..cccb7ed 100644
--- a/frontend/src/lib/resultsAccounting.test.ts
+++ b/frontend/src/lib/resultsAccounting.test.ts
@@ -53,7 +53,7 @@ const fetchMockOnce = () => (url) => {
};
describe('results accounting labels + values (live browser accounting)', () => {
- it('renders actual result DOM: 平仓回合数 label, trade turnover values, metrics grid classes', async () => {
+ it('renders actual result DOM: 平仓回合数 label, trade turnover values, metrics grid classes', { timeout: 20000 }, async () => {
// jsdom lacks ResizeObserver; Chart.svelte needs it only for responsive
// canvas resize, which jsdom never triggers. Narrow environment shim.
vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} });