diff --git a/client/src/webpages/dashboard/investigation/ItemInvestigation.tsx b/client/src/webpages/dashboard/investigation/ItemInvestigation.tsx index c0ae2937c..e3176c547 100644 --- a/client/src/webpages/dashboard/investigation/ItemInvestigation.tsx +++ b/client/src/webpages/dashboard/investigation/ItemInvestigation.tsx @@ -436,6 +436,7 @@ export default function ItemInvestigation(props: { /> diff --git a/client/src/webpages/dashboard/investigation/ItemInvestigationRuleResults.test.tsx b/client/src/webpages/dashboard/investigation/ItemInvestigationRuleResults.test.tsx new file mode 100644 index 000000000..69373d75c --- /dev/null +++ b/client/src/webpages/dashboard/investigation/ItemInvestigationRuleResults.test.tsx @@ -0,0 +1,110 @@ +import { render, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { MemoryRouter } from 'react-router-dom'; +import { describe, expect, it, vi } from 'vitest'; + +import { useGQLInvestigationItemsQuery } from '../../../graphql/generated'; +import ItemInvestigationRuleResults from './ItemInvestigationRuleResults'; + +vi.mock('../../../graphql/generated', async (importOriginal) => ({ + ...(await importOriginal()), + useGQLInvestigationItemsQuery: vi.fn(), + useGQLMatchingBankNamesQuery: vi.fn(() => ({ + loading: false, + error: undefined, + data: undefined, + })), +})); + +const execution = ( + outcome: 'PASSED' | 'FAILED', + timestamp: string, + conditionDetail: string, +) => ({ + __typename: 'RuleExecutionResult', + date: timestamp, + ts: timestamp, + contentId: 'content', + itemTypeName: 'Post', + itemTypeId: 'post', + content: '{}', + environment: 'LIVE', + passed: outcome === 'PASSED', + ruleId: 'repeated-rule', + ruleName: 'Repeated Rule', + policies: [], + tags: [], + result: { + __typename: 'ConditionSetWithResult', + conjunction: 'AND', + conditions: [ + { + __typename: 'LeafConditionWithResult', + input: { + __typename: 'ConditionInputField', + type: 'CONTENT_FIELD', + name: conditionDetail, + }, + comparator: 'EQUALS', + result: { __typename: 'ConditionResult', outcome }, + }, + ], + result: { __typename: 'ConditionResult', outcome }, + }, +}); + +describe('ItemInvestigationRuleResults', () => { + it('shows the stored result for the selected execution', () => { + vi.mocked(useGQLInvestigationItemsQuery).mockReturnValue({ + loading: false, + error: undefined, + data: { + __typename: 'Query', + itemWithHistory: { + __typename: 'ItemHistoryResult', + item: { + __typename: 'ContentItem', + id: 'item', + submissionId: 'submission', + type: { __typename: 'ContentItemType', id: 'post' }, + }, + executions: [ + execution( + 'FAILED', + '2026-01-01T20:11:00.000Z', + 'Older execution field', + ), + execution( + 'PASSED', + '2026-01-01T20:13:00.000Z', + 'Newer execution field', + ), + ], + }, + }, + } as unknown as ReturnType); + + render( + + + , + ); + + const failedRow = screen + .getAllByRole('row') + .find((row) => within(row).queryByText('Did Not Match'))!; + userEvent.click(failedRow); + + const dialog = screen.getByRole('dialog'); + expect(within(dialog).getByText('Rule Result: Repeated Rule')).toBeTruthy(); + const outcome = within(dialog).getByText('Outcome:').parentElement; + expect(outcome?.textContent).toContain('Did Not Match'); + expect(outcome?.textContent).not.toContain('Matched'); + expect(within(dialog).getByText('Older execution field')).toBeTruthy(); + expect(within(dialog).queryByText('Newer execution field')).toBeNull(); + }); +}); diff --git a/client/src/webpages/dashboard/investigation/ItemInvestigationRuleResults.tsx b/client/src/webpages/dashboard/investigation/ItemInvestigationRuleResults.tsx index 9a6cdb919..8304c2855 100644 --- a/client/src/webpages/dashboard/investigation/ItemInvestigationRuleResults.tsx +++ b/client/src/webpages/dashboard/investigation/ItemInvestigationRuleResults.tsx @@ -24,41 +24,48 @@ import Table from '../components/table/Table'; import { GQLConditionOutcome, + GQLInvestigationItemsQuery, + GQLItemType, useGQLInvestigationItemsQuery, } from '../../../graphql/generated'; import { ReadonlyDeep } from '../../../utils/typescript-types'; -import { LookbackVersion } from '../rules/info/insights/RuleInsightsSamplesTable'; -import RuleInsightsSampleDetailResults, { +import { getDisplayName, outcomeIcon, + RuleInsightsSampleDetailResultsImpl, } from '../rules/info/insights/sample_details/RuleInsightsSampleDetailResults'; +import type { ConditionSetWithResult } from '../rules/types'; import InvestigationTag from './InvestigationTag'; +type InvestigationExecution = Extract< + GQLInvestigationItemsQuery['itemWithHistory'], + { readonly __typename: 'ItemHistoryResult' } +>['executions'][number]; +type InvestigationExecutionResult = InvestigationExecution['result']; + export default function ItemInvestigationRuleResults(props: { itemIdentifier: ItemIdentifier; + itemTypes: readonly GQLItemType[]; submissionTime?: string; rules: Readonly[]>; }) { - const { rules, itemIdentifier, submissionTime } = props; + const { rules, itemIdentifier, submissionTime, itemTypes } = props; const navigate = useNavigate(); const [modalInfo, setModalInfo] = useState< | { visible: false; title: undefined; - ruleId: undefined; - contentId: undefined; + result: undefined; } | { visible: true; title: string; - ruleId: string; - contentId: string; + result: InvestigationExecutionResult; } >({ visible: false, title: undefined, - ruleId: undefined, - contentId: undefined, + result: undefined, }); const { @@ -250,8 +257,7 @@ export default function ItemInvestigationRuleResults(props: { setModalInfo({ visible: false, title: undefined, - ruleId: undefined, - contentId: undefined, + result: undefined, }); const modal = ( @@ -262,28 +268,27 @@ export default function ItemInvestigationRuleResults(props: { > {modalInfo.visible && (
- + {modalInfo.result ? ( + + ) : ( +
Rule result is unavailable
+ )}
)} ); const onSelectRow = (rowData: Row) => { - const executionResult = ruleExecutionsHistory[rowData.index]; - if (executionResult == null) { - return; - } - setModalInfo({ visible: true, title: `Rule Result: ${rowData.original.rule}`, - ruleId: executionResult.ruleId, - contentId: executionResult.contentId, + result: rowData.original.ruleExecutionResult, }); };