diff --git a/vehicle-data-platform/apps/web/src/pages/Quality.tsx b/vehicle-data-platform/apps/web/src/pages/Quality.tsx index 719e83c0..58e75635 100644 --- a/vehicle-data-platform/apps/web/src/pages/Quality.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Quality.tsx @@ -43,6 +43,10 @@ function qualityParams(values: Record) { return params; } +function qualityShareURL() { + return `${window.location.origin}${window.location.pathname}${window.location.hash}`; +} + async function copyText(value: string, label: string) { const text = value.trim(); if (!text) { @@ -57,6 +61,10 @@ async function copyText(value: string, label: string) { } } +async function copyQualityShareURL() { + await copyText(qualityShareURL(), '筛选链接'); +} + export function Quality({ onOpenVehicle, onHealthLoaded, @@ -194,6 +202,7 @@ export function Quality({ + diff --git a/vehicle-data-platform/apps/web/src/test/App.test.tsx b/vehicle-data-platform/apps/web/src/test/App.test.tsx index 679172f1..f174e757 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -854,6 +854,71 @@ test('applies shareable quality filters from hash', async () => { expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/quality/summary?keyword=%E7%B2%A4A&protocol=VEHICLE_SERVICE&issueType=NO_SOURCE'), undefined); }); +test('copies shareable quality filter link', async () => { + window.history.replaceState(null, '', '/#/quality?keyword=%E7%B2%A4A&protocol=VEHICLE_SERVICE&issueType=NO_SOURCE'); + const writeText = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: { writeText } + }); + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const path = String(input); + if (path.includes('/api/ops/health')) { + return { + ok: true, + json: async () => ({ + data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/quality/summary')) { + return { + ok: true, + json: async () => ({ + data: { + issueVehicleCount: 1, + issueRecordCount: 1, + errorCount: 0, + warningCount: 1, + protocols: [{ name: 'VEHICLE_SERVICE', count: 1 }], + issueTypes: [{ name: 'NO_SOURCE', count: 1 }] + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + if (path.includes('/api/quality/issues')) { + return { + ok: true, + json: async () => ({ + data: { items: [], total: 1, limit: 20, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + return { + ok: true, + json: async () => ({ + data: { items: [], total: 0, limit: 20, offset: 0 }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + fireEvent.click(await screen.findByRole('button', { name: /复制筛选链接/ })); + + await waitFor(() => { + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('/#/quality?keyword=%E7%B2%A4A&protocol=VEHICLE_SERVICE&issueType=NO_SOURCE')); + }); +}); + test('opens vehicle detail from shareable hash', async () => { window.history.replaceState(null, '', '/#/detail?keyword=%E7%B2%A4AG18312'); vi.spyOn(globalThis, 'fetch').mockResolvedValue({ diff --git a/vehicle-data-platform/apps/web/src/test/setup.ts b/vehicle-data-platform/apps/web/src/test/setup.ts index 87aeaacc..9fd89c5e 100644 --- a/vehicle-data-platform/apps/web/src/test/setup.ts +++ b/vehicle-data-platform/apps/web/src/test/setup.ts @@ -45,6 +45,13 @@ class TestResizeObserver { Object.defineProperty(window, 'ResizeObserver', { value: TestResizeObserver }); Object.defineProperty(globalThis, 'ResizeObserver', { value: TestResizeObserver }); +Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: { + writeText: () => Promise.resolve() + } +}); + Object.defineProperty(window, 'matchMedia', { value: (query: string) => ({ matches: false,