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('../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(/重新获取失败/); }); });