diff --git a/frontend/src/components/Operations/OperationDetailPage.test.tsx b/frontend/src/components/Operations/OperationDetailPage.test.tsx index 908bde9acc..4d0a11c56a 100644 --- a/frontend/src/components/Operations/OperationDetailPage.test.tsx +++ b/frontend/src/components/Operations/OperationDetailPage.test.tsx @@ -1,7 +1,7 @@ import { act, render, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { FluentProvider, webLightTheme } from '@fluentui/react-components' -import { MemoryRouter, Route, Routes } from 'react-router' +import { MemoryRouter, Route, Routes, useLocation } from 'react-router' import { attacksApi, operationsApi, scenariosApi } from '@/services/api' import type { Finding, FindingListItem, Operation, OperationDetail } from '@/types' @@ -242,6 +242,37 @@ it('shows the operation and records a finding within it', async () => { }) }) +it('does not push a duplicate history entry when a finding is saved on the first page', async () => { + const user = userEvent.setup() + const seenLocations: string[] = [] + function LocationProbe() { + const location = useLocation() + const marker = `${location.pathname}${location.search}|${location.key}` + if (seenLocations[seenLocations.length - 1] !== marker) { seenLocations.push(marker) } + return null + } + render( + + + + + } /> + + + , + ) + expect(await screen.findByRole('heading', { level: 1, name: OPERATION.name })).toBeInTheDocument() + await screen.findByText(/no findings/i) + await user.click(screen.getByRole('button', { name: 'New finding' })) + await user.type(screen.getByRole('textbox', { name: 'Title' }), FINDING.title) + jest.mocked(operationsApi.listFindings).mockResolvedValue({ items: [FINDING], has_more: false, next_offset: null }) + await user.click(screen.getByRole('button', { name: 'Save finding' })) + expect(await screen.findByRole('heading', { level: 2, name: FINDING.title })).toBeInTheDocument() + + expect(seenLocations).toHaveLength(1) + expect(seenLocations[0]?.startsWith('/operations/op-1')).toBe(true) +}) + it('links an optional evidence attack when the finding is created, but not when editing', async () => { const user = userEvent.setup() jest.mocked(operationsApi.listFindings).mockResolvedValue({ items: [FINDING], has_more: false, next_offset: null }) diff --git a/frontend/src/components/Operations/OperationDetailPage.tsx b/frontend/src/components/Operations/OperationDetailPage.tsx index d327beabb8..ccc67f273f 100644 --- a/frontend/src/components/Operations/OperationDetailPage.tsx +++ b/frontend/src/components/Operations/OperationDetailPage.tsx @@ -95,6 +95,9 @@ function OperationDetailContent({ operationId, onOperationChanged }: { operation }, [operationId, offset, requestKey, evidenceRevision]) const navigatePage = (nextOffset: number): void => { + // Writing the same offset again would push a duplicate history entry for an + // identical URL, so a save or delete on the first page must not navigate. + if ((nextOffset > 0 ? String(nextOffset) : null) === params.get('offset')) { return } setParams(nextOffset > 0 ? { offset: String(nextOffset) } : {}) }