fix(web): isolate paginated filter scopes

This commit is contained in:
lingniu
2026-07-16 04:44:21 +08:00
parent d95d7ab735
commit 14525887ea
7 changed files with 137 additions and 10 deletions

View File

@@ -1,7 +1,8 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, expect, test, vi } from 'vitest';
import { MemoryRouter } from 'react-router-dom';
import type { AlertEvent, Page } from '../../api/types';
import AlertsPage from './AlertsPage';
import { ROUTER_FUTURE } from '../routing/routerConfig';
@@ -18,6 +19,14 @@ afterEach(() => {
Object.values(mocks).forEach((mock) => mock.mockReset());
});
function alertEvent(id: string, vin: string, plate: string): AlertEvent {
return {
id, ruleId: 'speed-rule', ruleName: `${plate}速度告警`, ruleVersion: 1, severity: 'major', status: 'unprocessed',
vin, plate, protocol: 'JT808', metric: 'speed_kmh', operator: 'gt', triggerValue: 90, threshold: 80, thresholdHigh: 0,
unit: 'km/h', durationSec: 60, location: '测试位置', sourceEventId: `${id}-source`, eventAt: '2026-07-16T04:00:00Z', receivedAt: '2026-07-16T04:00:01Z', triggeredAt: '2026-07-16T04:00:00Z', recoveredAt: '', handler: '', version: 1, actions: []
};
}
test('preserves an unsubmitted event filter draft across alert tab URL changes', async () => {
mocks.alertSummaryV2.mockResolvedValue({ active: 0, unprocessed: 0, processing: 0, recovered: 0, closed: 0, ignored: 0, unreadNotifications: 0, asOf: '' });
mocks.alertEventsV2.mockResolvedValue({ items: [], total: 0, limit: 20, offset: 0 });
@@ -62,3 +71,33 @@ test('loads one full notification query on a direct notifications entry', async
expect(mocks.alertNotificationsV2).toHaveBeenCalledTimes(1);
expect(mocks.alertNotificationsV2.mock.calls[0][0].toString()).toBe('limit=100');
});
test('clears old alert rows and inspector evidence when the event scope changes', async () => {
const oldEvent = alertEvent('old-event', 'OLDVIN', '旧告警车牌');
const newEvent = alertEvent('new-event', 'NEWVIN', '新告警车牌');
let resolveNew!: (value: Page<AlertEvent>) => void;
mocks.alertSummaryV2.mockResolvedValue({ active: 1, unprocessed: 1, processing: 0, recovered: 0, closed: 0, ignored: 0, unreadNotifications: 0, asOf: '' });
mocks.alertEventsV2.mockImplementation((query: { keyword?: string }) => query.keyword === 'OLDVIN'
? Promise.resolve({ items: [oldEvent], total: 1, limit: 20, offset: 0 })
: new Promise<Page<AlertEvent>>((resolve) => { resolveNew = resolve; }));
mocks.alertEventV2.mockImplementation((id: string) => Promise.resolve(id === oldEvent.id ? oldEvent : newEvent));
mocks.alertRulesV2.mockResolvedValue([]);
mocks.alertNotificationsV2.mockResolvedValue({ items: [], total: 0, limit: 100, offset: 0 });
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/alerts?keyword=OLDVIN']}><AlertsPage /></MemoryRouter></QueryClientProvider>);
expect((await screen.findAllByText('旧告警车牌')).length).toBeGreaterThan(0);
expect(await screen.findByText('old-event')).toBeInTheDocument();
fireEvent.change(screen.getByPlaceholderText('车牌 / VIN / 规则名称'), { target: { value: 'NEWVIN' } });
fireEvent.click(screen.getByRole('button', { name: '查询' }));
expect(await screen.findByText('正在更新事件…')).toBeInTheDocument();
expect(screen.queryByText('旧告警车牌')).not.toBeInTheDocument();
expect(screen.queryByText('old-event')).not.toBeInTheDocument();
expect(screen.getByText('选择告警事件')).toBeInTheDocument();
await act(async () => resolveNew({ items: [newEvent], total: 1, limit: 20, offset: 0 }));
expect((await screen.findAllByText('新告警车牌')).length).toBeGreaterThan(0);
expect(await screen.findByText('new-event')).toBeInTheDocument();
await waitFor(() => expect(screen.queryByText('正在更新事件…')).not.toBeInTheDocument());
});