diff --git a/apps/dsa-web/src/pages/DataCenterPage.tsx b/apps/dsa-web/src/pages/DataCenterPage.tsx index 132b3bdb8..062b322df 100644 --- a/apps/dsa-web/src/pages/DataCenterPage.tsx +++ b/apps/dsa-web/src/pages/DataCenterPage.tsx @@ -1,5 +1,5 @@ import type React from 'react'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Database, RefreshCw, Settings2, TriangleAlert } from 'lucide-react'; import { Link } from 'react-router-dom'; import { dataCapabilityApi } from '../api/dataCapability'; @@ -54,26 +54,34 @@ const DataCenterPage: React.FC = () => { const [overview, setOverview] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); + const requestGeneration = useRef(0); useEffect(() => { document.title = `${t('dataCenter.title')} - DSA`; }, [t]); const load = useCallback(async () => { + const generation = ++requestGeneration.current; setLoading(true); setError(''); try { - setOverview(await dataCapabilityApi.getOverview()); + const nextOverview = await dataCapabilityApi.getOverview(); + if (generation !== requestGeneration.current) return; + setOverview(nextOverview); } catch { + if (generation !== requestGeneration.current) return; setOverview(null); setError(t('dataCenter.loadError')); } finally { - setLoading(false); + if (generation === requestGeneration.current) setLoading(false); } }, [t]); useEffect(() => { void load(); + return () => { + requestGeneration.current += 1; + }; }, [load]); const summary = useMemo(() => { diff --git a/apps/dsa-web/src/pages/__tests__/DataCenterPage.test.tsx b/apps/dsa-web/src/pages/__tests__/DataCenterPage.test.tsx index 2f153bdb1..4dd2cfbe9 100644 --- a/apps/dsa-web/src/pages/__tests__/DataCenterPage.test.tsx +++ b/apps/dsa-web/src/pages/__tests__/DataCenterPage.test.tsx @@ -1,6 +1,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { UiLanguageProvider, useUiLanguage } from '../../contexts/UiLanguageContext'; import DataCenterPage from '../DataCenterPage'; const { getOverview } = vi.hoisted(() => ({ getOverview: vi.fn() })); @@ -93,6 +94,21 @@ const overview = { warnings: ['screening_health_unknown'], }; +function LanguageSwitch() { + const { language, setLanguage } = useUiLanguage(); + return ; +} + +function deferred() { + let resolve!: (value: T) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise; + reject = rejectPromise; + }); + return { promise, resolve, reject }; +} + beforeEach(() => { vi.clearAllMocks(); getOverview.mockResolvedValue(overview); @@ -137,4 +153,28 @@ describe('DataCenterPage', () => { expect(await screen.findByText('暂无能力数据')).toBeInTheDocument(); await waitFor(() => expect(getOverview).toHaveBeenCalledTimes(2)); }); + + it('ignores an older request that finishes after a language-triggered reload', async () => { + const first = deferred(); + const second = deferred(); + getOverview.mockReset().mockReturnValueOnce(first.promise).mockReturnValueOnce(second.promise); + + render( + + + + , + ); + + await waitFor(() => expect(getOverview).toHaveBeenCalledTimes(1)); + fireEvent.click(screen.getByRole('button', { name: 'switch language' })); + await waitFor(() => expect(getOverview).toHaveBeenCalledTimes(2)); + + second.resolve(overview); + expect(await screen.findByText('AkShare')).toBeInTheDocument(); + first.reject(new Error('stale failure')); + + await waitFor(() => expect(screen.queryByText('Failed to load data overview')).not.toBeInTheDocument()); + expect(screen.getByText('AkShare')).toBeInTheDocument(); + }); });