diff --git a/src/web/components/form/radio.jsx b/src/web/components/form/radio.jsx index 01f998469b..508fde0ea4 100644 --- a/src/web/components/form/radio.jsx +++ b/src/web/components/form/radio.jsx @@ -30,7 +30,7 @@ const Radio = ({ {...props} checked={checked} disabled={disabled} - label={title} + label={String(title)} name={name} value={value} onChange={handleChange} diff --git a/src/web/pages/nvts/__tests__/nvtpreference.jsx b/src/web/pages/nvts/__tests__/nvtpreference.jsx new file mode 100644 index 0000000000..33627f0b33 --- /dev/null +++ b/src/web/pages/nvts/__tests__/nvtpreference.jsx @@ -0,0 +1,199 @@ +/* SPDX-FileCopyrightText: 2025 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, test, expect, testing} from '@gsa/testing'; +import NvtPreference from 'web/pages/nvts/nvtpreference'; +import {render, fireEvent, screen} from 'web/utils/testing'; + +describe('NvtPreference', () => { + const mockOnChange = testing.fn(); + + const renderComponent = (preference, value) => { + render( + , + ); + }; + + test('renders checkbox input', () => { + const preference = { + type: 'checkbox', + hr_name: 'Checkbox Preference', + name: 'checkbox_preference', + }; + renderComponent(preference, 'yes'); + + expect(screen.getByText('Checkbox Preference')).toBeVisible(); + expect(screen.getByRole('radio', {name: /yes/i})).toBeVisible(); + expect(screen.getByRole('radio', {name: /no/i})).toBeVisible(); + }); + + test('renders password input', () => { + const preference = { + type: 'password', + hr_name: 'Password Preference', + name: 'password_preference', + }; + renderComponent(preference, ''); + + expect(screen.getByText('Password Preference')).toBeVisible(); + expect( + screen.getByRole('checkbox', {name: /Replace existing password with/i}), + ).toBeVisible(); + expect(screen.getByTestId('password-input')).toBeDisabled(); + }); + + test('renders file input', () => { + const preference = { + type: 'file', + hr_name: 'File Preference', + name: 'file_preference', + value: '', + }; + renderComponent(preference, ''); + + expect(screen.getByText('File Preference')).toBeVisible(); + expect(screen.getByRole('checkbox', {name: /Upload file/i})).toBeVisible(); + expect(screen.getByTestId('file-input')).toBeDisabled(); + }); + + test('renders radio input', () => { + const preference = { + type: 'radio', + hr_name: 'Radio Preference', + name: 'radio_preference', + value: 'option1', + alt: ['option2', 'option3'], + }; + renderComponent(preference, 'option1'); + + expect(screen.getByText('Radio Preference')).toBeVisible(); + expect(screen.getByRole('radio', {name: /option1/i})).toBeChecked(); + expect(screen.getByRole('radio', {name: /option2/i})).toBeVisible(); + expect(screen.getByRole('radio', {name: /option3/i})).toBeVisible(); + }); + + test('renders radio input with numeric value 0', () => { + const preference = { + type: 'radio', + hr_name: 'Radio Preference', + name: 'radio_preference', + value: 1, + alt: [0, 2], + }; + renderComponent(preference, 2); + + expect(screen.getByText('Radio Preference')).toBeVisible(); + expect(screen.getByRole('radio', {name: '0'})).toBeVisible(); + expect(screen.getByRole('radio', {name: '1'})).toBeVisible(); + expect(screen.getByRole('radio', {name: '2'})).toBeChecked(); + + fireEvent.click(screen.getByRole('radio', {name: '0'})); + expect(mockOnChange).toHaveBeenCalledWith({ + type: 'setValue', + newState: {name: 'radio_preference', value: 0}, + }); + }); + + test('renders text input', () => { + const preference = { + type: 'text', + hr_name: 'Text Preference', + name: 'text_preference', + }; + renderComponent(preference, 'some text'); + + expect(screen.getByText('Text Preference')).toBeVisible(); + expect(screen.getByRole('textbox')).toHaveValue('some text'); + }); + + test('calls onChange when checkbox is toggled', () => { + const preference = { + type: 'checkbox', + hr_name: 'Checkbox Preference', + name: 'checkbox_preference', + }; + renderComponent(preference, 'yes'); + + fireEvent.click(screen.getByRole('radio', {name: /no/i})); + expect(mockOnChange).toHaveBeenCalledWith({ + type: 'setValue', + newState: {name: 'checkbox_preference', value: 'no'}, + }); + }); + + test('calls onChange when password checkbox is toggled', () => { + const preference = { + type: 'password', + hr_name: 'Password Preference', + name: 'password_preference', + }; + renderComponent(preference, ''); + + fireEvent.click( + screen.getByRole('checkbox', {name: /Replace existing password with/i}), + ); + + expect(screen.getByTestId('password-input')).not.toBeDisabled(); + expect(mockOnChange).toHaveBeenCalledWith({ + type: 'setValue', + newState: {name: 'password_preference', value: ''}, + }); + }); + + test('calls onChange when file checkbox is toggled', () => { + const preference = { + type: 'file', + hr_name: 'File Preference', + name: 'file_preference', + value: '', + }; + renderComponent(preference, ''); + + fireEvent.click(screen.getByRole('checkbox', {name: /Upload file/i})); + expect(mockOnChange).toHaveBeenCalledWith({ + type: 'setValue', + newState: {name: 'file_preference', value: ''}, + }); + }); + + test('calls onChange when radio is selected', () => { + const preference = { + type: 'radio', + hr_name: 'Radio Preference', + name: 'radio_preference', + value: 'option1', + alt: ['option2', 'option3'], + }; + renderComponent(preference, 'option1'); + + fireEvent.click(screen.getByRole('radio', {name: /option2/i})); + expect(mockOnChange).toHaveBeenCalledWith({ + type: 'setValue', + newState: {name: 'radio_preference', value: 'option2'}, + }); + }); + + test('calls onChange when text input is changed', () => { + const preference = { + type: 'text', + hr_name: 'Text Preference', + name: 'text_preference', + }; + renderComponent(preference, 'some text'); + + fireEvent.change(screen.getByRole('textbox'), { + target: {value: 'new text'}, + }); + expect(mockOnChange).toHaveBeenCalledWith({ + type: 'setValue', + newState: {name: 'text_preference', value: 'new text'}, + }); + }); +}); diff --git a/src/web/pages/nvts/nvtpreference.jsx b/src/web/pages/nvts/nvtpreference.jsx index 51c94eafe5..e3e194274d 100644 --- a/src/web/pages/nvts/nvtpreference.jsx +++ b/src/web/pages/nvts/nvtpreference.jsx @@ -6,7 +6,8 @@ import _ from 'gmp/locale'; import {map} from 'gmp/utils/array'; import {isEmpty} from 'gmp/utils/string'; -import React from 'react'; +import {useState} from 'react'; +import styled from 'styled-components'; import Checkbox from 'web/components/form/checkbox'; import FileField from 'web/components/form/filefield'; import PasswordField from 'web/components/form/passwordfield'; @@ -15,127 +16,116 @@ import TextField from 'web/components/form/textfield'; import YesNoRadio from 'web/components/form/yesnoradio'; import Column from 'web/components/layout/column'; import Divider from 'web/components/layout/divider'; -import Layout from 'web/components/layout/layout'; import TableData from 'web/components/table/data'; import TableRow from 'web/components/table/row'; import PropTypes from 'web/utils/proptypes'; -const noop_convert = value => value; +const StyledTableData = styled(TableData)` + overflow-wrap: break-word; + white-space: normal; + word-break: break-word; +`; -class NvtPreference extends React.Component { - constructor(...args) { - super(...args); +const noopConvert = value => value; - this.state = { - checked: false, - }; +const NvtPreference = ({preference, value = '', onChange}) => { + const [checked, setChecked] = useState(false); - this.onCheckedChange = this.onCheckedChange.bind(this); - this.onPreferenceChange = this.onPreferenceChange.bind(this); - } - - onPreferenceChange(value) { - const {onChange, preference} = this.props; + const onPreferenceChange = value => { onChange({type: 'setValue', newState: {name: preference.name, value}}); - } + }; - onCheckedChange(value) { + const onCheckedChange = value => { if (value) { - this.onPreferenceChange(''); + onPreferenceChange(''); } else { - this.onPreferenceChange(undefined); + onPreferenceChange(undefined); } - this.setState({checked: value}); - } + setChecked(value); + }; + const {type} = preference; - render() { - const {preference, value = ''} = this.props; + let input; - const {checked} = this.state; - const {type} = preference; - - let input; - - if (type === 'checkbox') { - input = ( - + ); + } else if (type === 'password') { + input = ( + + - ); - } else if (type === 'password') { - input = ( - - - - - ); - } else if (type === 'file') { - input = ( - - - - - ); - } else if (type === 'radio') { - input = ( - - - {map(preference.alt, alt => { - return ( - - ); - })} - - ); - } else { - input = ( - - ); - } - return ( - - {preference.hr_name} - {input} - {preference.default} - + + ); + } else if (type === 'file') { + input = ( + + + + + ); + } else if (type === 'radio') { + input = ( + + onPreferenceChange(preference.value)} + /> + {map(preference.alt, alt => { + return ( + onPreferenceChange(alt)} + /> + ); + })} + + ); + } else { + input = ( + ); } -} + return ( + + {preference.hr_name} + {input} + {preference.default} + + ); +}; NvtPreference.propTypes = { preference: PropTypes.shape({ diff --git a/src/web/pages/scanconfigs/editnvtdetailsdialog.jsx b/src/web/pages/scanconfigs/editnvtdetailsdialog.jsx index 07cf4e7151..16b030f276 100644 --- a/src/web/pages/scanconfigs/editnvtdetailsdialog.jsx +++ b/src/web/pages/scanconfigs/editnvtdetailsdialog.jsx @@ -10,6 +10,7 @@ import DateTime from 'web/components/date/datetime'; import SaveDialog from 'web/components/dialog/savedialog'; import Radio from 'web/components/form/radio'; import TextField from 'web/components/form/textfield'; +import Column from 'web/components/layout/column'; import Divider from 'web/components/layout/divider'; import Layout from 'web/components/layout/layout'; import DetailsLink from 'web/components/link/detailslink'; @@ -227,7 +228,7 @@ const EditNvtDetailsDialog = ({ {_('Timeout')} - + - - setDefaultTimeout(value)} - /> - - - + setDefaultTimeout(value)} + /> + + {isDefined(defaultTimeout) ? defaultTimeout : ''}