From 074fce23fddf5a4af87f89a39feaeafe78ad1f99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ricks?= Date: Tue, 29 Sep 2026 08:44:08 +0200 Subject: [PATCH 01/11] Change: Remove transforming data from DataDisplay This change will make all DataDisplay much easier to understand. They just render some specific kind of data. No transformed data no transformed props. --- .../dashboard/display/DataDisplay.tsx | 36 +++------- .../dashboard/display/DataTableDisplay.tsx | 47 +++---------- .../display/__tests__/DataDisplay.test.tsx | 67 +++++++------------ .../__tests__/DataTableDisplay.test.tsx | 13 ++-- 4 files changed, 50 insertions(+), 113 deletions(-) diff --git a/src/web/components/dashboard/display/DataDisplay.tsx b/src/web/components/dashboard/display/DataDisplay.tsx index 0459b64d64..eb84928403 100644 --- a/src/web/components/dashboard/display/DataDisplay.tsx +++ b/src/web/components/dashboard/display/DataDisplay.tsx @@ -22,9 +22,6 @@ import DisplayContainer, { DISPLAY_HEADER_HEIGHT, DISPLAY_BORDER_WIDTH, } from 'web/components/dashboard/display/DisplayContainer'; -import useDataTransform, { - type TransformFunc, -} from 'web/components/dashboard/display/useDataTransform'; import IconDivider from 'web/components/layout/IconDivider'; import Layout from 'web/components/layout/Layout'; import useTranslation from 'web/hooks/useTranslation'; @@ -69,15 +66,12 @@ type DataDisplayChildren< export type DataDisplayProps< TData extends object, - TTransformedData extends object, - TTransformProps extends object = object, TState extends DisplayState = DisplayState, - TChildren = DataDisplayChildren, + TChildren = DataDisplayChildren, > = Omit, 'children' | 'title'> & { - data?: TData; - dataRow?: DataRowFunc; + data: TData | undefined; + dataRow?: DataRowFunc; dataTitles?: DataTitles; - dataTransform: TransformFunc; filter?: FilterType; icons?: IconsRenderFunc; children?: TChildren; @@ -87,8 +81,8 @@ export type DataDisplayProps< showFilterString?: boolean; showSvgDownload?: boolean; showToggleLegend?: boolean; - title: ToString | TitleFunc; -} & TTransformProps; + title: ToString | TitleFunc; +}; const log = logger.getLogger('web.components.dashboard.display.DataDisplay'); @@ -168,21 +162,13 @@ const createSvgUrl = ( const DataDisplay = < TData extends object, - TProps extends DataDisplayProps< - TData, - TTransformedData, - TTransformProps, - TState - >, - TTransformedData extends object, - TTransformProps extends object = object, + TProps extends DataDisplayProps, TState extends DisplayState = DisplayState, >({ children, data, dataRow, dataTitles, - dataTransform, dragHandleRef, filter, height, @@ -206,10 +192,8 @@ const DataDisplay = < const downloadSvgUrlRef = useRef(undefined); const downloadCsvUrlRef = useRef(undefined); - const originalData = data; - const transformedData = useDataTransform(originalData, dataTransform); const title = isFunction(titleFunc) - ? titleFunc({data: transformedData, isLoading}) + ? titleFunc({data, isLoading}) : String(titleFunc); const getCurrentState = (newState: TState | undefined = state): TState => { @@ -274,7 +258,7 @@ const DataDisplay = < return; } - const rowData = dataRow(transformedData); + const rowData = dataRow(data); const csvData = [ escapeCsv(title), dataTitles.map(t => escapeCsv(String(t))).join(','), @@ -301,7 +285,7 @@ const DataDisplay = < height = height - DISPLAY_HEADER_HEIGHT; width = width - DISPLAY_BORDER_WIDTH; - isLoading = isLoading && !isDefined(originalData); + isLoading = isLoading && !isDefined(data); const showCsvDownload = isDefined(dataRow) && isDefined(dataTitles); @@ -328,7 +312,7 @@ const DataDisplay = < {isFunction(children) ? // oxlint-disable-next-line react/refs children({ - data: transformedData, + data, width, height, svgRef, diff --git a/src/web/components/dashboard/display/DataTableDisplay.tsx b/src/web/components/dashboard/display/DataTableDisplay.tsx index 769926a1e8..d25b2ca26a 100644 --- a/src/web/components/dashboard/display/DataTableDisplay.tsx +++ b/src/web/components/dashboard/display/DataTableDisplay.tsx @@ -12,63 +12,34 @@ import DataTable, { type DataTableProps, } from 'web/components/dashboard/display/DataTable'; -type DataTableDisplayRenderProps = - DataTableProps & { - data?: TTransformedData; +type DataTableDisplayRenderProps = + DataTableProps & { + data?: TData; }; -type DataTableDisplayChildren = ( - props: DataTableDisplayRenderProps, +type DataTableDisplayChildren = ( + props: DataTableDisplayRenderProps, ) => React.ReactNode; export type DataTableDisplayProps< TData extends object, - TTransformedData extends object, - TTransformProps extends object = object, TState extends DisplayState = DisplayState, -> = DataDisplayProps< - TData, - TTransformedData, - TTransformProps, - TState, - DataTableDisplayChildren ->; +> = DataDisplayProps>; type DataTableDisplayComponentProps< TData extends object, - TTransformedData extends object, - TTransformProps extends object, TState extends DisplayState, -> = DataTableDisplayProps & - TTransformProps; +> = DataTableDisplayProps; const DataTableDisplay = < TData extends object, - TTransformedData extends object, - TTransformProps extends object = object, TState extends DisplayState = DisplayState, >( - props: DataTableDisplayComponentProps< - TData, - TTransformedData, - TTransformProps, - TState - >, + props: DataTableDisplayComponentProps, ) => { const {children, dataRow, dataTitles} = props; return ( - , - TTransformedData, - TTransformProps, - TState - > + , TState> {...props} showSvgDownload={false} showToggleLegend={false} diff --git a/src/web/components/dashboard/display/__tests__/DataDisplay.test.tsx b/src/web/components/dashboard/display/__tests__/DataDisplay.test.tsx index 91174f4dc0..8073792108 100644 --- a/src/web/components/dashboard/display/__tests__/DataDisplay.test.tsx +++ b/src/web/components/dashboard/display/__tests__/DataDisplay.test.tsx @@ -30,17 +30,16 @@ interface TestState extends DisplayState { showLegend?: boolean; } -type TestProps = DataDisplayProps & +type TestProps = DataDisplayProps & Pick; const createProps = (overrides: Partial = {}): TestProps => ({ children: testing.fn(({data}) => ( -
{data[0].value}
+
{data?.[0]?.value ?? 'empty'}
)), - data: {value: 'raw'}, + data: [{value: 'raw'}], dataRow: rows => rows?.map(row => [row.value]) ?? [], dataTitles: ['Value'], - dataTransform: data => [{value: `${data?.value}-transformed`}], height: 100, icons: () =>
, initialState: {showLegend: false}, @@ -66,21 +65,19 @@ describe('DataDisplay component tests', () => { testing.restoreAllMocks(); }); - test('should transform data and render the chart with its title', () => { + test('should render the data and chart with its title', () => { const props = createProps(); - render( - {...props} />, - ); + render( {...props} />); - expect(screen.getByText('chart-1: raw-transformed')).toBeInTheDocument(); - expect(screen.getByTestId('chart')).toHaveTextContent('raw-transformed'); + expect(screen.getByText('chart-1: raw')).toBeInTheDocument(); + expect(screen.getByTestId('chart')).toHaveTextContent('raw'); expect(screen.getByTestId('icons')).toBeInTheDocument(); }); test('should render a string title', () => { render( - + {...createProps({title: 'Static title'})} />, ); @@ -92,14 +89,11 @@ describe('DataDisplay component tests', () => { const children = testing.fn(() =>
); const props = createProps({ children, - data: undefined as unknown as TestData, - dataTransform: () => [], + data: undefined, isLoading: true, }); - render( - {...props} />, - ); + render( {...props} />); expect(children).not.toHaveBeenCalled(); expect(screen.getByTestId('loading')).toBeVisible(); @@ -110,17 +104,16 @@ describe('DataDisplay component tests', () => { const children = testing.fn(() =>
); const props = createProps({ children, - data: undefined as unknown as TestData, - dataTransform: () => [], + data: undefined, }); const {rerender} = render( - {...props} />, + {...props} />, ); expect(screen.getByTestId('chart')).toBeInTheDocument(); rerender( - + {...props} isLoading={true} />, @@ -139,9 +132,7 @@ describe('DataDisplay component tests', () => { showToggleLegend: false, }); - render( - {...props} />, - ); + render( {...props} />); expect(icons).toHaveBeenCalledTimes(1); expect(icons).toHaveBeenCalledWith( @@ -175,14 +166,14 @@ describe('DataDisplay component tests', () => { showFilterString: true, }); const {rerender} = render( - {...props} />, + {...props} />, ); expect(screen.getByText('First filter')).toBeInTheDocument(); expect(screen.getByText('foo=one')).toBeInTheDocument(); rerender( - + {...props} filter={secondFilter} showFilterString={true} @@ -200,27 +191,25 @@ describe('DataDisplay component tests', () => { terms: parseFilterTermsFromString('foo=one'), }); const children = testing.fn(({data}) => ( -
{data[0].value}
+
{data?.[0]?.value ?? 'empty'}
)); const props = createProps({children, filter: firstFilter}); const {rerender} = render( - {...props} />, + {...props} />, ); expect(children).toHaveBeenCalledTimes(1); - expect(screen.getByTestId('chart')).toHaveTextContent('raw-transformed'); + expect(screen.getByTestId('chart')).toHaveTextContent('raw'); rerender( - + {...props} - data={{value: 'changed'}} + data={[{value: 'changed'}]} />, ); expect(children).toHaveBeenCalledTimes(2); - expect(screen.getByTestId('chart')).toHaveTextContent( - 'changed-transformed', - ); + expect(screen.getByTestId('chart')).toHaveTextContent('changed'); }); test('should create a CSV download with escaped data', async () => { @@ -228,15 +217,13 @@ describe('DataDisplay component tests', () => {
), })); @@ -154,6 +158,40 @@ describe('HostsModifiedHighDisplay', () => { expect(screen.queryByText(/7\|12/)).toBeNull(); }); }); + + test('should call onFilterChanged with a modification date range filter', async () => { + const onFilterChanged = testing.fn(); + const filter = QueryFilter.fromString('severity=high'); + + renderDisplay( + , + ); + + const rangeButton = await screen.findByRole('button', { + name: 'select-range', + }); + fireEvent.click(rangeButton); + + expect(onFilterChanged).toHaveBeenCalledTimes(1); + const newFilter = onFilterChanged.mock.calls[0][0]; + expect(newFilter.toFilterString()).toContain('severity=high'); + expect(newFilter.toFilterString()).toContain('modified>2026-01-01t00:00'); + expect(newFilter.toFilterString()).toContain('modified<2026-01-03t00:00'); + }); + + test('should not throw when selecting a range without onFilterChanged', async () => { + renderDisplay(); + + const rangeButton = await screen.findByRole('button', { + name: 'select-range', + }); + expect(() => fireEvent.click(rangeButton)).not.toThrow(); + }); }); describe('HostsModifiedHighTableDisplay', () => { diff --git a/src/web/pages/hosts/dashboard/__tests__/HostsTopologyDisplay.test.tsx b/src/web/pages/hosts/dashboard/__tests__/HostsTopologyDisplay.test.tsx index e61908bbb0..479489e0e4 100644 --- a/src/web/pages/hosts/dashboard/__tests__/HostsTopologyDisplay.test.tsx +++ b/src/web/pages/hosts/dashboard/__tests__/HostsTopologyDisplay.test.tsx @@ -24,10 +24,11 @@ const loaderData = [ ]; vi.mock('web/components/dashboard/display/DataDisplay', () => ({ - default: ({children, data, title}) => { + default: ({children, data, showToggleLegend, title}) => { return (
{title?.({data})} + {String(showToggleLegend)} {typeof children === 'function' ? children({ width: 400, @@ -108,6 +109,9 @@ describe('HostsTopologyDisplay', () => { await waitFor(() => { expect(screen.getByTestId('title')).toHaveTextContent('Hosts Topology'); + expect(screen.getByTestId('show-toggle-legend')).toHaveTextContent( + 'false', + ); expect(screen.getByTestId('severity-rating')).toHaveTextContent('CVSSv3'); expect(screen.getByTestId('host-count')).toHaveTextContent('3'); expect(screen.getByTestId('link-count')).toHaveTextContent('2'); diff --git a/src/web/pages/hosts/dashboard/__tests__/HostsVulnScoreDisplay.test.tsx b/src/web/pages/hosts/dashboard/__tests__/HostsVulnScoreDisplay.test.tsx index 215ddabdc1..76c692efbb 100644 --- a/src/web/pages/hosts/dashboard/__tests__/HostsVulnScoreDisplay.test.tsx +++ b/src/web/pages/hosts/dashboard/__tests__/HostsVulnScoreDisplay.test.tsx @@ -5,7 +5,7 @@ import {type ReactElement} from 'react'; import {describe, expect, test, testing} from '@gsa/testing'; -import {rendererWith, screen, waitFor} from 'web/testing'; +import {fireEvent, rendererWith, screen, waitFor} from 'web/testing'; import {SEVERITY_RATING_CVSS_3} from 'gmp/utils/severity'; import {getDisplay} from 'web/components/dashboard/registry'; import { @@ -38,7 +38,14 @@ const loaderData = { }; vi.mock('web/components/dashboard/display/DataDisplay', () => ({ - default: ({children, data, dataTransform, severityRating, title}) => { + default: ({ + children, + data, + dataTransform, + severityRating, + showToggleLegend, + title, + }) => { const transformedData = dataTransform ? dataTransform(data, {severityRating}) : data; @@ -46,6 +53,7 @@ vi.mock('web/components/dashboard/display/DataDisplay', () => ({ return (
{title?.({data: transformedData})} + {String(showToggleLegend)} {typeof children === 'function' ? children({ width: 400, @@ -113,9 +121,12 @@ const createGmp = () => ({ }, }); -const renderDisplay = (component: ReactElement) => { +const renderDisplay = ( + component: ReactElement, + {showLocation}: {showLocation?: boolean} = {}, +) => { const subscribe: SubscribeFunc = testing.fn().mockReturnValue(testing.fn()); - const {render} = rendererWith({gmp: createGmp()}); + const {render} = rendererWith({gmp: createGmp(), showLocation}); return render( @@ -151,6 +162,9 @@ describe('HostsVulnScoreDisplay', () => { expect(screen.getByTestId('title')).toHaveTextContent( 'Most Vulnerable Hosts', ); + expect(screen.getByTestId('show-toggle-legend')).toHaveTextContent( + 'false', + ); expect( screen.getByRole('button', {name: 'bar-Host Three'}), ).toBeVisible(); @@ -160,18 +174,12 @@ describe('HostsVulnScoreDisplay', () => { }); test('should navigate to the selected host', async () => { - const subscribe: SubscribeFunc = testing.fn().mockReturnValue(testing.fn()); - const {render} = rendererWith({gmp: createGmp(), showLocation: true}); - render( - - - , - ); + renderDisplay(, { + showLocation: true, + }); - await waitFor(() => - expect(screen.getByRole('button', {name: 'bar-Host One'})).toBeVisible(), - ); - screen.getByRole('button', {name: 'bar-Host One'}).click(); + const button = await screen.findByRole('button', {name: 'bar-Host One'}); + fireEvent.click(button); await waitFor(() => expect(screen.getByTestId('location-pathname')).toHaveTextContent( diff --git a/src/web/pages/notes/dashboard/__tests__/NoteActiveDaysDisplay.test.tsx b/src/web/pages/notes/dashboard/__tests__/NoteActiveDaysDisplay.test.tsx index d363777093..30a8869a17 100644 --- a/src/web/pages/notes/dashboard/__tests__/NoteActiveDaysDisplay.test.tsx +++ b/src/web/pages/notes/dashboard/__tests__/NoteActiveDaysDisplay.test.tsx @@ -5,7 +5,7 @@ import {type ReactElement} from 'react'; import {describe, expect, test, testing} from '@gsa/testing'; -import {rendererWith, screen, waitFor} from 'web/testing'; +import {fireEvent, rendererWith, screen, waitFor} from 'web/testing'; import {getDisplay} from 'web/components/dashboard/registry'; import { SubscriptionContext, @@ -61,12 +61,16 @@ vi.mock('web/components/dashboard/display/DataTableDisplay', () => ({ })); vi.mock('web/components/chart/DonutChart', () => ({ - default: ({data}) => ( + default: ({data, onDataClick}) => (
{data.map((row, index) => ( - + ))}
), @@ -127,6 +131,38 @@ describe('NotesActiveDaysDisplay', () => { ); }); }); + + test('should call onFilterChanged with an active-days filter', async () => { + const onFilterChanged = testing.fn(); + + renderDisplay( + , + ); + + const activeDaysButton = await screen.findByRole('button', { + name: /Active \(unlimited\)/, + }); + fireEvent.click(activeDaysButton); + + expect(onFilterChanged).toHaveBeenCalledTimes(1); + expect(onFilterChanged.mock.calls[0][0].toFilterString()).toBe( + 'active_days="-2"', + ); + }); + + test('should not throw when clicking a data item without onFilterChanged', async () => { + renderDisplay(); + + const activeDaysButton = await screen.findByRole('button', { + name: /Active \(unlimited\)/, + }); + + expect(() => fireEvent.click(activeDaysButton)).not.toThrow(); + }); }); describe('NotesActiveDaysTableDisplay', () => { diff --git a/src/web/pages/notes/dashboard/__tests__/NoteWordCloudDisplay.test.tsx b/src/web/pages/notes/dashboard/__tests__/NoteWordCloudDisplay.test.tsx index 5add1d83a5..b55066beed 100644 --- a/src/web/pages/notes/dashboard/__tests__/NoteWordCloudDisplay.test.tsx +++ b/src/web/pages/notes/dashboard/__tests__/NoteWordCloudDisplay.test.tsx @@ -5,7 +5,7 @@ import {type ReactElement} from 'react'; import {describe, expect, test, testing} from '@gsa/testing'; -import {rendererWith, screen, waitFor} from 'web/testing'; +import {fireEvent, rendererWith, screen, waitFor} from 'web/testing'; import {getDisplay} from 'web/components/dashboard/registry'; import { SubscriptionContext, @@ -24,10 +24,11 @@ const loaderData = { }; vi.mock('web/components/dashboard/display/DataDisplay', () => ({ - default: ({children, data, title}) => { + default: ({children, data, showToggleLegend, title}) => { return (
{title?.(data)} + {String(showToggleLegend)} {typeof children === 'function' ? children({ width: 400, @@ -59,12 +60,16 @@ vi.mock('web/components/dashboard/display/DataTableDisplay', () => ({ })); vi.mock('web/components/chart/WordCloudChart', () => ({ - default: ({data}) => ( + default: ({data, onDataClick}) => (
{data.map((row, index) => ( - + ))}
), @@ -122,6 +127,45 @@ describe('NotesWordCloudDisplay', () => { ); }); }); + + test('should set showToggleLegend to false', async () => { + renderDisplay(); + + expect(await screen.findByTestId('show-toggle-legend')).toHaveTextContent( + 'false', + ); + }); + + test('should call onFilterChanged with a text filter when clicking a word', async () => { + const onFilterChanged = testing.fn(); + + renderDisplay( + , + ); + + const wordButton = await screen.findByRole('button', { + name: /security\|5\|security/, + }); + fireEvent.click(wordButton); + + expect(onFilterChanged).toHaveBeenCalledTimes(1); + expect(onFilterChanged.mock.calls[0][0].toFilterString()).toBe( + 'text~"security"', + ); + }); + + test('should not throw when clicking a word without onFilterChanged', async () => { + renderDisplay(); + + const wordButton = await screen.findByRole('button', { + name: /security\|5\|security/, + }); + expect(() => fireEvent.click(wordButton)).not.toThrow(); + }); }); describe('NotesWordCloudTableDisplay', () => { diff --git a/src/web/pages/nvts/dashboard/__tests__/NvtFamilyDisplay.test.tsx b/src/web/pages/nvts/dashboard/__tests__/NvtFamilyDisplay.test.tsx index a7c276a0d5..13d96a7031 100644 --- a/src/web/pages/nvts/dashboard/__tests__/NvtFamilyDisplay.test.tsx +++ b/src/web/pages/nvts/dashboard/__tests__/NvtFamilyDisplay.test.tsx @@ -5,7 +5,7 @@ import {type ReactElement} from 'react'; import {describe, expect, test, testing} from '@gsa/testing'; -import {rendererWith, screen, waitFor} from 'web/testing'; +import {fireEvent, rendererWith, screen, waitFor} from 'web/testing'; import QueryFilter from 'gmp/models/filter/query-filter'; import {SEVERITY_RATING_CVSS_3} from 'gmp/utils/severity'; import {getDisplay} from 'web/components/dashboard/registry'; @@ -26,10 +26,11 @@ const loaderData = { }; vi.mock('web/components/dashboard/display/DataDisplay', () => ({ - default: ({children, data, title}) => { + default: ({children, data, showToggleLegend, title}) => { return (
{title?.({data})} + {String(showToggleLegend)} {typeof children === 'function' ? children({ width: 400, @@ -121,6 +122,9 @@ describe('NvtsFamilyDisplay', () => { ); expect(screen.getByRole('button', {name: 'Linux'})).toBeInTheDocument(); expect(screen.getByRole('button', {name: 'Windows'})).toBeInTheDocument(); + expect(screen.getByTestId('show-toggle-legend')).toHaveTextContent( + 'false', + ); }); }); @@ -135,7 +139,7 @@ describe('NvtsFamilyDisplay', () => { ); const family = await screen.findByRole('button', {name: 'Linux'}); - family.click(); + fireEvent.click(family); expect(onFilterChanged).toHaveBeenCalledTimes(1); expect(onFilterChanged.mock.calls[0][0].toFilterString()).toBe( @@ -155,10 +159,17 @@ describe('NvtsFamilyDisplay', () => { ); const family = await screen.findByRole('button', {name: 'Linux'}); - family.click(); + fireEvent.click(family); expect(onFilterChanged).not.toHaveBeenCalled(); }); + + test('should not throw when clicking a family without onFilterChanged', async () => { + renderDisplay(); + + const family = await screen.findByRole('button', {name: 'Linux'}); + expect(() => fireEvent.click(family)).not.toThrow(); + }); }); describe('NvtsFamilyTableDisplay', () => { diff --git a/src/web/pages/nvts/dashboard/__tests__/NvtQodDisplay.test.tsx b/src/web/pages/nvts/dashboard/__tests__/NvtQodDisplay.test.tsx index 0a907866be..d8b474884a 100644 --- a/src/web/pages/nvts/dashboard/__tests__/NvtQodDisplay.test.tsx +++ b/src/web/pages/nvts/dashboard/__tests__/NvtQodDisplay.test.tsx @@ -5,7 +5,7 @@ import {type ReactElement} from 'react'; import {describe, expect, test, testing} from '@gsa/testing'; -import {rendererWith, screen, waitFor} from 'web/testing'; +import {fireEvent, rendererWith, screen, waitFor} from 'web/testing'; import QueryFilter from 'gmp/models/filter/query-filter'; import {getDisplay} from 'web/components/dashboard/registry'; import { @@ -133,7 +133,7 @@ describe('NvtsQodDisplay', () => { ); const qod = await screen.findByRole('button', {name: '80 %'}); - qod.click(); + fireEvent.click(qod); expect(onFilterChanged).toHaveBeenCalledTimes(1); expect(onFilterChanged.mock.calls[0][0].toFilterString()).toBe('qod="80"'); @@ -151,10 +151,17 @@ describe('NvtsQodDisplay', () => { ); const qod = await screen.findByRole('button', {name: '80 %'}); - qod.click(); + fireEvent.click(qod); expect(onFilterChanged).not.toHaveBeenCalled(); }); + + test('should not throw when clicking a QoD without onFilterChanged', async () => { + renderDisplay(); + + const qod = await screen.findByRole('button', {name: '80 %'}); + expect(() => fireEvent.click(qod)).not.toThrow(); + }); }); describe('NvtsQodTableDisplay', () => { diff --git a/src/web/pages/nvts/dashboard/__tests__/NvtQodTypeDisplay.test.tsx b/src/web/pages/nvts/dashboard/__tests__/NvtQodTypeDisplay.test.tsx index 94a1bb3add..bea81cfeb9 100644 --- a/src/web/pages/nvts/dashboard/__tests__/NvtQodTypeDisplay.test.tsx +++ b/src/web/pages/nvts/dashboard/__tests__/NvtQodTypeDisplay.test.tsx @@ -5,7 +5,7 @@ import {type ReactElement} from 'react'; import {describe, expect, test, testing} from '@gsa/testing'; -import {rendererWith, screen, waitFor} from 'web/testing'; +import {fireEvent, rendererWith, screen, waitFor} from 'web/testing'; import QueryFilter from 'gmp/models/filter/query-filter'; import {getDisplay} from 'web/components/dashboard/registry'; import { @@ -133,7 +133,7 @@ describe('NvtsQodTypeDisplay', () => { ); const qodType = await screen.findByRole('button', {name: 'General note'}); - qodType.click(); + fireEvent.click(qodType); expect(onFilterChanged).toHaveBeenCalledTimes(1); expect(onFilterChanged.mock.calls[0][0].toFilterString()).toBe( @@ -153,10 +153,17 @@ describe('NvtsQodTypeDisplay', () => { ); const qodType = await screen.findByRole('button', {name: 'General note'}); - qodType.click(); + fireEvent.click(qodType); expect(onFilterChanged).not.toHaveBeenCalled(); }); + + test('should not throw when clicking a QoD-type without onFilterChanged', async () => { + renderDisplay(); + + const qodType = await screen.findByRole('button', {name: 'General note'}); + expect(() => fireEvent.click(qodType)).not.toThrow(); + }); }); describe('NvtsQodTypeTableDisplay', () => { diff --git a/src/web/pages/operatingsystems/dashboard/__tests__/OperatingSystemVulnerabilityScoreDisplay.test.tsx b/src/web/pages/operatingsystems/dashboard/__tests__/OperatingSystemVulnerabilityScoreDisplay.test.tsx index 7e64806fe4..1ee1711509 100644 --- a/src/web/pages/operatingsystems/dashboard/__tests__/OperatingSystemVulnerabilityScoreDisplay.test.tsx +++ b/src/web/pages/operatingsystems/dashboard/__tests__/OperatingSystemVulnerabilityScoreDisplay.test.tsx @@ -5,7 +5,7 @@ import {type ReactElement} from 'react'; import {describe, expect, test, testing} from '@gsa/testing'; -import {rendererWith, screen, waitFor} from 'web/testing'; +import {fireEvent, rendererWith, screen, waitFor} from 'web/testing'; import {SEVERITY_RATING_CVSS_3} from 'gmp/utils/severity'; import {getDisplay} from 'web/components/dashboard/registry'; import { @@ -47,10 +47,11 @@ const loaderData = { }; vi.mock('web/components/dashboard/display/DataDisplay', () => ({ - default: ({children, data, title}) => { + default: ({children, data, showToggleLegend, title}) => { return (
{title?.({data})} + {String(showToggleLegend)} {typeof children === 'function' ? children({ width: 400, @@ -110,9 +111,9 @@ const createGmp = () => ({ }, }); -const renderDisplay = (component: ReactElement) => { +const renderDisplay = (component: ReactElement, options = {}) => { const subscribe: SubscribeFunc = testing.fn().mockReturnValue(testing.fn()); - const {render} = rendererWith({gmp: createGmp(), store: true}); + const {render} = rendererWith({gmp: createGmp(), store: true, ...options}); return render( @@ -121,7 +122,7 @@ const renderDisplay = (component: ReactElement) => { ); }; -describe('OperatingSystemVulnerabilityScoreDisplay', () => { +describe('OperatingSystemVulnerabilityScoreDisplay tests', () => { test('should export a valid component with the correct configuration', () => { expect(OperatingSystemVulnerabilityScoreDisplay).toBeDefined(); expect(OperatingSystemVulnerabilityScoreDisplay.displayId).toBe( @@ -158,11 +159,32 @@ describe('OperatingSystemVulnerabilityScoreDisplay', () => { screen.getByRole('button', {name: 'Linux One'}), ).toBeInTheDocument(); expect(screen.queryByRole('button', {name: 'Linux Two'})).toBeNull(); + expect(screen.getByTestId('show-toggle-legend')).toHaveTextContent( + 'false', + ); + }); + }); + + test('should navigate to the selected operating system', async () => { + renderDisplay( + , + {showLocation: true}, + ); + + const operatingSystem = await screen.findByRole('button', { + name: 'Linux One', + }); + fireEvent.click(operatingSystem); + + await waitFor(() => { + expect(screen.getByTestId('location-pathname')).toHaveTextContent( + '/operating-system/os-1', + ); }); }); }); -describe('OsVulnScoreTableDisplay', () => { +describe('OperatingSystemVulnerabilityScoreTableDisplay tests', () => { test('should export a valid component with the correct configuration', () => { expect(OperatingSystemVulnerabilityScoreTableDisplay).toBeDefined(); expect(typeof OperatingSystemVulnerabilityScoreTableDisplay).toBe( diff --git a/src/web/pages/overrides/dashboard/__tests__/OverrideActiveDaysDisplay.test.tsx b/src/web/pages/overrides/dashboard/__tests__/OverrideActiveDaysDisplay.test.tsx index cdf43badf4..fd89d16469 100644 --- a/src/web/pages/overrides/dashboard/__tests__/OverrideActiveDaysDisplay.test.tsx +++ b/src/web/pages/overrides/dashboard/__tests__/OverrideActiveDaysDisplay.test.tsx @@ -5,7 +5,8 @@ import {type ReactElement} from 'react'; import {describe, expect, test, testing} from '@gsa/testing'; -import {rendererWith, screen, waitFor} from 'web/testing'; +import {fireEvent, rendererWith, screen, waitFor} from 'web/testing'; +import QueryFilter from 'gmp/models/filter/query-filter'; import {getDisplay} from 'web/components/dashboard/registry'; import { SubscriptionContext, @@ -61,13 +62,23 @@ vi.mock('web/components/dashboard/display/DataTableDisplay', () => ({ })); vi.mock('web/components/chart/DonutChart', () => ({ - default: ({data}) => ( + default: ({data, onDataClick}) => (
{data.map((row, index) => ( - + ))} +
), })); @@ -131,6 +142,74 @@ describe('OverridesActiveDaysDisplay', () => { ); }); }); + + test('should call onFilterChanged with an active-days filter', async () => { + const onFilterChanged = testing.fn(); + + renderDisplay( + , + ); + + const activeDaysButton = await screen.findByRole('button', { + name: /Active \(unlimited\)/, + }); + fireEvent.click(activeDaysButton); + + expect(onFilterChanged).toHaveBeenCalledTimes(1); + expect(onFilterChanged.mock.calls[0][0].toFilterString()).toBe( + 'active_days="-2"', + ); + }); + + test('should not throw when clicking a data item without onFilterChanged', async () => { + renderDisplay(); + + const activeDaysButton = await screen.findByRole('button', { + name: /Active \(unlimited\)/, + }); + expect(() => fireEvent.click(activeDaysButton)).not.toThrow(); + }); + + test('should not call onFilterChanged when the filter value is empty', async () => { + const onFilterChanged = testing.fn(); + + renderDisplay( + , + ); + + const emptyItem = await screen.findByTestId('empty-filter-value'); + fireEvent.click(emptyItem); + + expect(onFilterChanged).not.toHaveBeenCalled(); + }); + + test('should not call onFilterChanged when the filter already has the active-days term', async () => { + const onFilterChanged = testing.fn(); + + renderDisplay( + , + ); + + const activeDaysButton = await screen.findByRole('button', { + name: /Active \(unlimited\)/, + }); + fireEvent.click(activeDaysButton); + + expect(onFilterChanged).not.toHaveBeenCalled(); + }); }); describe('OverridesActiveDaysTableDisplay', () => { diff --git a/src/web/pages/overrides/dashboard/__tests__/OverrideWordCloudDisplay.test.tsx b/src/web/pages/overrides/dashboard/__tests__/OverrideWordCloudDisplay.test.tsx index f7018c1067..2757f529d4 100644 --- a/src/web/pages/overrides/dashboard/__tests__/OverrideWordCloudDisplay.test.tsx +++ b/src/web/pages/overrides/dashboard/__tests__/OverrideWordCloudDisplay.test.tsx @@ -5,7 +5,8 @@ import {type ReactElement} from 'react'; import {describe, expect, test, testing} from '@gsa/testing'; -import {rendererWith, screen, waitFor} from 'web/testing'; +import {fireEvent, rendererWith, screen, waitFor} from 'web/testing'; +import QueryFilter from 'gmp/models/filter/query-filter'; import {getDisplay} from 'web/components/dashboard/registry'; import { SubscriptionContext, @@ -24,10 +25,11 @@ const loaderData = { }; vi.mock('web/components/dashboard/display/DataDisplay', () => ({ - default: ({children, data, title}) => { + default: ({children, data, showToggleLegend, title}) => { return (
{title?.(data)} + {String(showToggleLegend)} {typeof children === 'function' ? children({ width: 400, @@ -59,13 +61,20 @@ vi.mock('web/components/dashboard/display/DataTableDisplay', () => ({ })); vi.mock('web/components/chart/WordCloudChart', () => ({ - default: ({data}) => ( + default: ({data, onDataClick}) => (
{data.map((row, index) => ( - + ))} +
), })); @@ -124,6 +133,81 @@ describe('OverridesWordCloudDisplay', () => { ); }); }); + + test('should set showToggleLegend to false', async () => { + renderDisplay(); + + expect(await screen.findByTestId('show-toggle-legend')).toHaveTextContent( + 'false', + ); + }); + + test('should call onFilterChanged with a text filter when clicking a word', async () => { + const onFilterChanged = testing.fn(); + + renderDisplay( + , + ); + + const wordButton = await screen.findByRole('button', { + name: /security\|5\|security/, + }); + fireEvent.click(wordButton); + + expect(onFilterChanged).toHaveBeenCalledTimes(1); + expect(onFilterChanged.mock.calls[0][0].toFilterString()).toBe( + 'text~"security"', + ); + }); + + test('should not call onFilterChanged when the filter already has the word term', async () => { + const onFilterChanged = testing.fn(); + + renderDisplay( + , + ); + + const wordButton = await screen.findByRole('button', { + name: /security\|5\|security/, + }); + fireEvent.click(wordButton); + + expect(onFilterChanged).not.toHaveBeenCalled(); + }); + + test('should not call onFilterChanged when the word is empty', async () => { + const onFilterChanged = testing.fn(); + + renderDisplay( + , + ); + + fireEvent.click(await screen.findByTestId('empty-filter-value')); + + expect(onFilterChanged).not.toHaveBeenCalled(); + }); + + test('should not throw when clicking a word without onFilterChanged', async () => { + renderDisplay(); + + const wordButton = await screen.findByRole('button', { + name: /security\|5\|security/, + }); + expect(() => fireEvent.click(wordButton)).not.toThrow(); + }); }); describe('OverridesWordCloudTableDisplay', () => { diff --git a/src/web/pages/reports/dashboard/__tests__/ReportHighResultsDisplay.test.tsx b/src/web/pages/reports/dashboard/__tests__/ReportHighResultsDisplay.test.tsx index 175f53169f..f08975ba6f 100644 --- a/src/web/pages/reports/dashboard/__tests__/ReportHighResultsDisplay.test.tsx +++ b/src/web/pages/reports/dashboard/__tests__/ReportHighResultsDisplay.test.tsx @@ -5,7 +5,8 @@ import {type ReactElement} from 'react'; import {describe, expect, test, testing} from '@gsa/testing'; -import {rendererWith, screen, waitFor} from 'web/testing'; +import {fireEvent, rendererWith, screen, waitFor} from 'web/testing'; +import QueryFilter from 'gmp/models/filter/query-filter'; import {getDisplay} from 'web/components/dashboard/registry'; import { SubscriptionContext, @@ -72,7 +73,7 @@ vi.mock('web/components/dashboard/display/DataTableDisplay', () => ({ })); vi.mock('web/components/chart/LineChart', () => ({ - default: ({data, xAxisLabel, yAxisLabel, y2AxisLabel}) => ( + default: ({data, onRangeSelected, xAxisLabel, yAxisLabel, y2AxisLabel}) => (
{xAxisLabel} {yAxisLabel} @@ -82,6 +83,9 @@ vi.mock('web/components/chart/LineChart', () => ({ {row.label}|{row.y}|{row.y2} ))} +
), })); @@ -143,6 +147,37 @@ describe('ReportsHighResultsDisplay', () => { expect(screen.getByTestId('data-point-1')).toHaveTextContent('|7|3.5'); }); }); + + test('should call onFilterChanged with a date range filter', async () => { + const onFilterChanged = testing.fn(); + const filter = QueryFilter.fromString('severity=high'); + + renderDisplay( + , + ); + + const button = await screen.findByRole('button', {name: 'select-range'}); + fireEvent.click(button); + + expect(onFilterChanged).toHaveBeenCalledTimes(1); + const newFilter = onFilterChanged.mock.calls[0][0]; + expect(newFilter.toFilterString()).toContain('severity=high'); + expect(newFilter.toFilterString()).toContain('date>2026-01-01t00:00'); + expect(newFilter.toFilterString()).toContain('date<2026-01-02t00:00'); + }); + + test('should not throw when selecting a range without onFilterChanged', async () => { + renderDisplay(); + + const button = await screen.findByRole('button', {name: 'select-range'}); + + expect(() => fireEvent.click(button)).not.toThrow(); + }); }); describe('ReportsHighResultsTableDisplay', () => { diff --git a/src/web/pages/results/dashboard/__tests__/ResultDescriptionWordCloudDisplay.test.tsx b/src/web/pages/results/dashboard/__tests__/ResultDescriptionWordCloudDisplay.test.tsx index 7c29c6c91b..4159aa59e5 100644 --- a/src/web/pages/results/dashboard/__tests__/ResultDescriptionWordCloudDisplay.test.tsx +++ b/src/web/pages/results/dashboard/__tests__/ResultDescriptionWordCloudDisplay.test.tsx @@ -5,7 +5,8 @@ import {type ReactElement} from 'react'; import {describe, expect, test, testing} from '@gsa/testing'; -import {rendererWith, screen, waitFor} from 'web/testing'; +import {fireEvent, rendererWith, screen, waitFor} from 'web/testing'; +import QueryFilter from 'gmp/models/filter/query-filter'; import {getDisplay} from 'web/components/dashboard/registry'; import { SubscriptionContext, @@ -24,12 +25,13 @@ const loaderData = { }; vi.mock('web/components/dashboard/display/DataDisplay', () => ({ - default: ({children, data, dataTransform, title}) => { + default: ({children, data, dataTransform, showToggleLegend, title}) => { const transformedData = dataTransform ? dataTransform(data) : data; return (
{title?.()} + {String(showToggleLegend)} {typeof children === 'function' ? children({ width: 400, @@ -61,13 +63,23 @@ vi.mock('web/components/dashboard/display/DataTableDisplay', () => ({ })); vi.mock('web/components/chart/WordCloudChart', () => ({ - default: ({data}) => ( + default: ({data, onDataClick}) => (
{data.map((row, index) => ( - + ))} +
), })); @@ -134,6 +146,79 @@ describe('ResultsDescriptionWordCloudDisplay', () => { ); }); }); + + test('should set showToggleLegend to false', async () => { + renderDisplay( + , + ); + + expect(await screen.findByTestId('show-toggle-legend')).toHaveTextContent( + 'false', + ); + }); + + test('should call onFilterChanged with a description filter when clicking a word', async () => { + const onFilterChanged = testing.fn(); + + renderDisplay( + , + ); + + fireEvent.click(await screen.findByTestId('data-word-0')); + + expect(onFilterChanged).toHaveBeenCalledTimes(1); + expect(onFilterChanged.mock.calls[0][0].toFilterString()).toBe( + 'description~"security issue"', + ); + }); + + test('should not add a description filter that already exists', async () => { + const onFilterChanged = testing.fn(); + const filter = QueryFilter.fromString('description~"security issue"'); + + renderDisplay( + , + ); + + fireEvent.click(await screen.findByTestId('data-word-0')); + + expect(onFilterChanged).not.toHaveBeenCalled(); + }); + + test('should ignore an empty word', async () => { + const onFilterChanged = testing.fn(); + + renderDisplay( + , + ); + + fireEvent.click(await screen.findByTestId('empty-filter-value')); + + expect(onFilterChanged).not.toHaveBeenCalled(); + }); + + test('should not throw when clicking a word without onFilterChanged', async () => { + renderDisplay( + , + ); + + const button = await screen.findByTestId('data-word-0'); + + expect(() => fireEvent.click(button)).not.toThrow(); + }); }); describe('ResultsDescriptionWordCloudTableDisplay', () => { diff --git a/src/web/pages/results/dashboard/__tests__/ResultWordCloudDisplay.test.tsx b/src/web/pages/results/dashboard/__tests__/ResultWordCloudDisplay.test.tsx index 21587321c5..684e7795e1 100644 --- a/src/web/pages/results/dashboard/__tests__/ResultWordCloudDisplay.test.tsx +++ b/src/web/pages/results/dashboard/__tests__/ResultWordCloudDisplay.test.tsx @@ -5,7 +5,8 @@ import {type ReactElement} from 'react'; import {describe, expect, test, testing} from '@gsa/testing'; -import {rendererWith, screen, waitFor} from 'web/testing'; +import {fireEvent, rendererWith, screen, waitFor} from 'web/testing'; +import QueryFilter from 'gmp/models/filter/query-filter'; import {getDisplay} from 'web/components/dashboard/registry'; import { SubscriptionContext, @@ -24,10 +25,11 @@ const loaderData = { }; vi.mock('web/components/dashboard/display/DataDisplay', () => ({ - default: ({children, data, title}) => { + default: ({children, data, showToggleLegend, title}) => { return (
{title?.()} + {String(showToggleLegend)} {typeof children === 'function' ? children({ width: 400, @@ -59,13 +61,23 @@ vi.mock('web/components/dashboard/display/DataTableDisplay', () => ({ })); vi.mock('web/components/chart/WordCloudChart', () => ({ - default: ({data}) => ( + default: ({data, onDataClick}) => (
{data.map((row, index) => ( - + ))} +
), })); @@ -124,6 +136,75 @@ describe('ResultsWordCloudDisplay', () => { ); }); }); + + test('should set showToggleLegend to false', async () => { + renderDisplay(); + + expect(await screen.findByTestId('show-toggle-legend')).toHaveTextContent( + 'false', + ); + }); + + test('should call onFilterChanged with a vulnerability filter when clicking a word', async () => { + const onFilterChanged = testing.fn(); + + renderDisplay( + , + ); + + fireEvent.click(await screen.findByTestId('data-word-0')); + + expect(onFilterChanged).toHaveBeenCalledTimes(1); + expect(onFilterChanged.mock.calls[0][0].toFilterString()).toBe( + 'vulnerability~"security"', + ); + }); + + test('should not add a vulnerability filter that already exists', async () => { + const onFilterChanged = testing.fn(); + const filter = QueryFilter.fromString('vulnerability~"security"'); + + renderDisplay( + , + ); + + fireEvent.click(await screen.findByTestId('data-word-0')); + + expect(onFilterChanged).not.toHaveBeenCalled(); + }); + + test('should ignore an empty word', async () => { + const onFilterChanged = testing.fn(); + + renderDisplay( + , + ); + + fireEvent.click(await screen.findByTestId('empty-filter-value')); + + expect(onFilterChanged).not.toHaveBeenCalled(); + }); + + test('should not throw when clicking a word without onFilterChanged', async () => { + renderDisplay(); + + const button = await screen.findByTestId('data-word-0'); + + expect(() => fireEvent.click(button)).not.toThrow(); + }); }); describe('ResultsWordCloudTableDisplay', () => { diff --git a/src/web/pages/tasks/dashboard/__tests__/TaskHighResultsDisplay.test.tsx b/src/web/pages/tasks/dashboard/__tests__/TaskHighResultsDisplay.test.tsx index 07fc851c34..b6153771bc 100644 --- a/src/web/pages/tasks/dashboard/__tests__/TaskHighResultsDisplay.test.tsx +++ b/src/web/pages/tasks/dashboard/__tests__/TaskHighResultsDisplay.test.tsx @@ -5,7 +5,7 @@ import {type ReactElement} from 'react'; import {describe, expect, test, testing} from '@gsa/testing'; -import {rendererWith, screen, waitFor} from 'web/testing'; +import {fireEvent, rendererWith, screen, waitFor} from 'web/testing'; import {SEVERITY_RATING_CVSS_3} from 'gmp/utils/severity'; import {getDisplay} from 'web/components/dashboard/registry'; import { @@ -35,10 +35,11 @@ const loaderData = { }; vi.mock('web/components/dashboard/display/DataDisplay', () => ({ - default: ({children, data, title}) => { + default: ({children, data, showToggleLegend, title}) => { return (
{title?.({data})} + {String(showToggleLegend)} {typeof children === 'function' ? children({ width: 400, @@ -70,12 +71,16 @@ vi.mock('web/components/dashboard/display/DataTableDisplay', () => ({ })); vi.mock('web/components/chart/BubbleChart', () => ({ - default: ({data}) => ( + default: ({data, onDataClick}) => (
{data.map((row, index) => ( - + ))}
), @@ -98,9 +103,16 @@ const createGmp = () => ({ }, }); -const renderDisplay = (component: ReactElement) => { +const renderDisplay = ( + component: ReactElement, + {showLocation}: {showLocation?: boolean} = {}, +) => { const subscribe: SubscribeFunc = testing.fn().mockReturnValue(testing.fn()); - const {render} = rendererWith({gmp: createGmp(), store: true}); + const {render} = rendererWith({ + gmp: createGmp(), + showLocation, + store: true, + }); return render( @@ -141,6 +153,29 @@ describe('TasksHighResultsDisplay', () => { expect(screen.queryByText(/Task Three/)).toBeNull(); }); }); + + test('should show the legend toggle as disabled', async () => { + renderDisplay(); + + expect(await screen.findByTestId('show-toggle-legend')).toHaveTextContent( + 'false', + ); + }); + + test('should navigate to the selected task', async () => { + renderDisplay(, { + showLocation: true, + }); + + const button = await screen.findByRole('button', {name: /Task One/}); + fireEvent.click(button); + + await waitFor(() => + expect(screen.getByTestId('location-pathname')).toHaveTextContent( + '/task/task-1', + ), + ); + }); }); describe('TasksHighResultsTableDisplay', () => { diff --git a/src/web/pages/tasks/dashboard/__tests__/TaskMostHighResultsDisplay.test.tsx b/src/web/pages/tasks/dashboard/__tests__/TaskMostHighResultsDisplay.test.tsx index 906cdcbe99..650d39170f 100644 --- a/src/web/pages/tasks/dashboard/__tests__/TaskMostHighResultsDisplay.test.tsx +++ b/src/web/pages/tasks/dashboard/__tests__/TaskMostHighResultsDisplay.test.tsx @@ -5,7 +5,7 @@ import {type ReactElement} from 'react'; import {describe, expect, test, testing} from '@gsa/testing'; -import {rendererWith, screen, waitFor} from 'web/testing'; +import {fireEvent, rendererWith, screen, waitFor} from 'web/testing'; import {SEVERITY_RATING_CVSS_3} from 'gmp/utils/severity'; import {getDisplay} from 'web/components/dashboard/registry'; import { @@ -35,10 +35,11 @@ const loaderData = { }; vi.mock('web/components/dashboard/display/DataDisplay', () => ({ - default: ({children, data, title}) => { + default: ({children, data, showToggleLegend, title}) => { return (
{title?.(data)} + {String(showToggleLegend)} {typeof children === 'function' ? children({ width: 400, @@ -70,13 +71,17 @@ vi.mock('web/components/dashboard/display/DataTableDisplay', () => ({ })); vi.mock('web/components/chart/BarChart', () => ({ - default: ({data, xLabel}) => ( + default: ({data, onDataClick, xLabel}) => (
{xLabel} {data.map((row, index) => ( - + ))}
), @@ -99,9 +104,16 @@ const createGmp = () => ({ }, }); -const renderDisplay = (component: ReactElement) => { +const renderDisplay = ( + component: ReactElement, + {showLocation}: {showLocation?: boolean} = {}, +) => { const subscribe: SubscribeFunc = testing.fn().mockReturnValue(testing.fn()); - const {render} = rendererWith({gmp: createGmp(), store: true}); + const {render} = rendererWith({ + gmp: createGmp(), + showLocation, + store: true, + }); return render( @@ -149,6 +161,29 @@ describe('TasksMostHighResultsDisplay', () => { expect(screen.queryByText(/Task Three/)).toBeNull(); }); }); + + test('should show the legend toggle as disabled', async () => { + renderDisplay(); + + expect(await screen.findByTestId('show-toggle-legend')).toHaveTextContent( + 'false', + ); + }); + + test('should navigate to the selected task', async () => { + renderDisplay(, { + showLocation: true, + }); + + const button = await screen.findByRole('button', {name: /Task One/}); + fireEvent.click(button); + + await waitFor(() => + expect(screen.getByTestId('location-pathname')).toHaveTextContent( + '/task/task-1', + ), + ); + }); }); describe('TasksMostHighResultsTableDisplay', () => { diff --git a/src/web/pages/tasks/dashboard/__tests__/TaskSchedulesDisplay.test.tsx b/src/web/pages/tasks/dashboard/__tests__/TaskSchedulesDisplay.test.tsx index ed16588e84..188432dbae 100644 --- a/src/web/pages/tasks/dashboard/__tests__/TaskSchedulesDisplay.test.tsx +++ b/src/web/pages/tasks/dashboard/__tests__/TaskSchedulesDisplay.test.tsx @@ -33,12 +33,13 @@ const scheduledTask = { const loaderData = [scheduledTask, {name: 'Task Without Schedule'}]; vi.mock('web/components/dashboard/display/DataDisplay', () => ({ - default: ({children, data, title}) => { + default: ({children, data, showToggleLegend, title}) => { return (
{isFunction(title) ? title({data}) : title} + {String(showToggleLegend)} {isFunction(children) ? children({ width: 400, @@ -127,6 +128,9 @@ describe('TasksSchedulesDisplay', () => { expect(screen.getByTestId('title')).toHaveTextContent( 'Next Scheduled Tasks', ); + expect(screen.getByTestId('show-toggle-legend')).toHaveTextContent( + 'false', + ); expect(screen.getByTestId('data-point-0')).toHaveTextContent( 'Scheduled Task|3600|0', ); diff --git a/src/web/pages/tickets/dashboard/__tests__/TicketUsersAssignedDisplay.test.tsx b/src/web/pages/tickets/dashboard/__tests__/TicketUsersAssignedDisplay.test.tsx index 3c7bacbc65..a3b2b541e2 100644 --- a/src/web/pages/tickets/dashboard/__tests__/TicketUsersAssignedDisplay.test.tsx +++ b/src/web/pages/tickets/dashboard/__tests__/TicketUsersAssignedDisplay.test.tsx @@ -26,10 +26,11 @@ const loaderData = [ ]; vi.mock('web/components/dashboard/display/DataDisplay', () => ({ - default: ({children, data, title}) => { + default: ({children, data, showToggleLegend, title}) => { return (
{title?.({data})} + {String(showToggleLegend)} {typeof children === 'function' ? children({ width: 400, @@ -122,6 +123,9 @@ describe('TicketsAssignedUsersDisplay', () => { expect(screen.getByTestId('title')).toHaveTextContent( 'Tickets by Assigned User (Total: 6)', ); + expect(screen.getByTestId('show-toggle-legend')).toHaveTextContent( + 'false', + ); expect(screen.getByTestId('data-point-Alice')).toHaveTextContent( 'Alice|2|Alice', ); diff --git a/src/web/pages/tlscertificates/dashboard/__tests__/TlsCertificateModifiedDisplay.test.tsx b/src/web/pages/tlscertificates/dashboard/__tests__/TlsCertificateModifiedDisplay.test.tsx index 2582938e0e..bba538f7bc 100644 --- a/src/web/pages/tlscertificates/dashboard/__tests__/TlsCertificateModifiedDisplay.test.tsx +++ b/src/web/pages/tlscertificates/dashboard/__tests__/TlsCertificateModifiedDisplay.test.tsx @@ -5,7 +5,8 @@ import {type ReactElement} from 'react'; import {describe, expect, test, testing} from '@gsa/testing'; -import {rendererWith, screen, waitFor} from 'web/testing'; +import {fireEvent, rendererWith, screen, waitFor} from 'web/testing'; +import QueryFilter from 'gmp/models/filter/query-filter'; import {getDisplay} from 'web/components/dashboard/registry'; import { SubscriptionContext, @@ -44,7 +45,7 @@ vi.mock('web/components/dashboard/display/DataDisplay', () => ({ })); vi.mock('web/components/chart/LineChart', () => ({ - default: ({data, xAxisLabel, yAxisLabel, y2AxisLabel}) => ( + default: ({data, onRangeSelected, xAxisLabel, yAxisLabel, y2AxisLabel}) => (
{xAxisLabel} {yAxisLabel} @@ -54,6 +55,9 @@ vi.mock('web/components/chart/LineChart', () => ({ {row.label}|{row.y}|{row.y2} ))} +
), })); @@ -140,6 +144,37 @@ describe('TlsCertificatesModifiedDisplay', () => { ); }); }); + + test('should call onFilterChanged with a modification date range filter', async () => { + const onFilterChanged = testing.fn(); + const filter = QueryFilter.fromString('severity=high'); + + renderDisplay( + , + ); + + const button = await screen.findByRole('button', {name: 'select-range'}); + fireEvent.click(button); + + expect(onFilterChanged).toHaveBeenCalledTimes(1); + const newFilter = onFilterChanged.mock.calls[0][0]; + expect(newFilter.toFilterString()).toContain('severity=high'); + expect(newFilter.toFilterString()).toContain('modified>2026-01-01t00:00'); + expect(newFilter.toFilterString()).toContain('modified<2026-01-02t00:00'); + }); + + test('should not throw when selecting a range without onFilterChanged', async () => { + renderDisplay(); + + const button = await screen.findByRole('button', {name: 'select-range'}); + + expect(() => fireEvent.click(button)).not.toThrow(); + }); }); describe('TlsCertificatesModifiedTableDisplay', () => { diff --git a/src/web/pages/vulnerabilities/dashboard/__tests__/VulnerabilitiesHostsDisplay.test.tsx b/src/web/pages/vulnerabilities/dashboard/__tests__/VulnerabilitiesHostsDisplay.test.tsx index fd61a93d76..f6d1b90156 100644 --- a/src/web/pages/vulnerabilities/dashboard/__tests__/VulnerabilitiesHostsDisplay.test.tsx +++ b/src/web/pages/vulnerabilities/dashboard/__tests__/VulnerabilitiesHostsDisplay.test.tsx @@ -5,7 +5,7 @@ import {type ReactElement} from 'react'; import {describe, expect, test, testing} from '@gsa/testing'; -import {rendererWith, screen} from 'web/testing'; +import {fireEvent, rendererWith, screen} from 'web/testing'; import QueryFilter from 'gmp/models/filter/query-filter'; import {getDisplay} from 'web/components/dashboard/registry'; import { @@ -127,7 +127,7 @@ describe('VulnerabilitiesHostsDisplay', () => { ); const bar = await screen.findByRole('button', {name: 'bar-4-5'}); - bar.click(); + fireEvent.click(bar); expect(onFilterChanged).toHaveBeenCalledTimes(1); const newFilter = onFilterChanged.mock.calls[0][0]; @@ -148,7 +148,7 @@ describe('VulnerabilitiesHostsDisplay', () => { ); const bar = await screen.findByRole('button', {name: 'bar-0'}); - bar.click(); + fireEvent.click(bar); expect(onFilterChanged).toHaveBeenCalledTimes(1); const newFilter = onFilterChanged.mock.calls[0][0]; @@ -170,7 +170,7 @@ describe('VulnerabilitiesHostsDisplay', () => { ); const bar = await screen.findByRole('button', {name: 'bar-0'}); - bar.click(); + fireEvent.click(bar); expect(onFilterChanged).not.toHaveBeenCalled(); }); @@ -183,7 +183,7 @@ describe('VulnerabilitiesHostsDisplay', () => { ); const bar = await screen.findByRole('button', {name: 'bar-0'}); - expect(() => bar.click()).not.toThrow(); + expect(() => fireEvent.click(bar)).not.toThrow(); }); }); From bb12df24b1e0e2e47ab0682bdc27cb8049d4de98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ricks?= Date: Wed, 30 Sep 2026 10:44:34 +0200 Subject: [PATCH 10/11] test: Update transformHostsData function for changed return data The return data is now an array with a total property. --- .../dashboard/__tests__/hosts-transform.test.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/web/pages/vulnerabilities/dashboard/__tests__/hosts-transform.test.ts b/src/web/pages/vulnerabilities/dashboard/__tests__/hosts-transform.test.ts index 3c27affe5f..fc262f8a96 100644 --- a/src/web/pages/vulnerabilities/dashboard/__tests__/hosts-transform.test.ts +++ b/src/web/pages/vulnerabilities/dashboard/__tests__/hosts-transform.test.ts @@ -8,11 +8,17 @@ import transformHostsData from 'web/pages/vulnerabilities/dashboard/hosts-transf describe('transformHostsData', () => { test('should return an empty array when no data is provided', () => { - expect(transformHostsData()).toEqual([]); + const result = transformHostsData(); + + expect(result).toHaveLength(0); + expect(result.total).toBe(0); }); test('should return no data for empty groups', () => { - expect(transformHostsData({groups: []})).toEqual([]); + const result = transformHostsData({groups: []}); + + expect(result).toHaveLength(0); + expect(result.total).toBe(0); }); test('should create a zero-host bucket for a single group', () => { @@ -21,6 +27,7 @@ describe('transformHostsData', () => { }); expect(result).toHaveLength(1); + expect(result.total).toBe(4); expect(result[0]).toMatchObject({ x: '0', y: 4, @@ -40,6 +47,7 @@ describe('transformHostsData', () => { }); expect(result).toHaveLength(3); + expect(result.total).toBe(5); expect(result.map(({x, y}) => ({x, y}))).toEqual([ {x: '0-1', y: 2}, {x: '2-3', y: 0}, @@ -60,6 +68,7 @@ describe('transformHostsData', () => { ], }); + expect(result.total).toBe(10); expect(result.map(({toolTip}) => toolTip)).toEqual([ '0 - 0: 2 (20.0%)', '1 - 1: 0 (0.0%)', From 8cfddfb5cc34644ff71939620cd03199becb9e9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ricks?= Date: Thu, 1 Oct 2026 09:48:36 +0200 Subject: [PATCH 11/11] Update SeverityClassDisplay to remove redundant type interface --- .../dashboard/display/severity/SeverityClassDisplay.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/web/components/dashboard/display/severity/SeverityClassDisplay.tsx b/src/web/components/dashboard/display/severity/SeverityClassDisplay.tsx index d320edf7a2..35037603fd 100644 --- a/src/web/components/dashboard/display/severity/SeverityClassDisplay.tsx +++ b/src/web/components/dashboard/display/severity/SeverityClassDisplay.tsx @@ -17,16 +17,14 @@ import { } from 'web/components/dashboard/display/severity/severity-class-transform'; import {filterValueToFilterTerms} from 'web/components/dashboard/display/severity/utils'; -type SeverityClassDisplayBaseProps = +type SeverityClassDataDisplayProps = DataDisplayProps; -interface SeverityClassDisplayProps extends SeverityClassDisplayBaseProps { +interface SeverityClassDisplayProps extends SeverityClassDataDisplayProps { filter?: FilterType; onFilterChanged?: (filter: FilterType) => void; } -type SeverityClassDataDisplayProps = SeverityClassDisplayBaseProps; - const SeverityClassDisplay = ({ onFilterChanged, filter,