diff --git a/webpack/components/ResourceQuotaForm/ResourceQuotaFormConstants.js b/webpack/components/ResourceQuotaForm/ResourceQuotaFormConstants.js index af9e9a9..68daf43 100644 --- a/webpack/components/ResourceQuotaForm/ResourceQuotaFormConstants.js +++ b/webpack/components/ResourceQuotaForm/ResourceQuotaFormConstants.js @@ -19,14 +19,14 @@ export const RESOURCE_NAME_DISK = 'Disk space'; /* Resource units (order the units with increasing factor!) */ export const RESOURCE_UNIT_CPU = [{ symbol: 'cores', factor: 1 }]; export const RESOURCE_UNIT_MEMORY = [ - { symbol: 'MB', factor: 1 }, - { symbol: 'GB', factor: 1024 }, - { symbol: 'TB', factor: 1024 * 1024 }, + { symbol: 'MiB', factor: 1 }, + { symbol: 'GiB', factor: 1024 }, + { symbol: 'TiB', factor: 1024 * 1024 }, ]; export const RESOURCE_UNIT_DISK = [ - { symbol: 'GB', factor: 1 }, - { symbol: 'TB', factor: 1024 }, - { symbol: 'PB', factor: 1024 * 1024 }, + { symbol: 'GiB', factor: 1 }, + { symbol: 'TiB', factor: 1024 }, + { symbol: 'PiB', factor: 1024 * 1024 }, ]; /* Resource value bounds */ diff --git a/webpack/components/ResourceQuotaForm/components/Resource/UnitInputField.js b/webpack/components/ResourceQuotaForm/components/Resource/UnitInputField.js index 7d4a198..8106fd3 100644 --- a/webpack/components/ResourceQuotaForm/components/Resource/UnitInputField.js +++ b/webpack/components/ResourceQuotaForm/components/Resource/UnitInputField.js @@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; import { FormGroup, + FormHelperText, TextInput, InputGroup, InputGroupText, @@ -60,17 +61,19 @@ const UnitInputField = ({ }, [minValue, maxValue, selectedUnit]); /* text for float errors */ - const errorTextNatural = useCallback( - () => __('Value must be a natural number.'), - [] - ); + const errorTextNatural = useCallback(() => __('Value must be a number.'), []); - /* text for float errors */ - const errorTextFloating = useCallback( - () => __(`No floating point for smallest unit (${units[0].symbol}).`), + /* text for float inputs (rounding) */ + const warningTextRounded = useCallback( + roundedValue => __(`Rounding to: ${roundedValue} (${units[0].symbol}).`), [units] ); + /* warning text displayed beneath value input field (built-in is used for errors) */ + const helperTextWarning = (text, isHidden) => ( + {text} + ); + /* applies the selected unit and checks the bounds */ const isValid = useCallback( val => { @@ -83,20 +86,9 @@ const UnitInputField = ({ setErrorText(errorTextBounds()); return false; } - if (baseValue !== Math.floor(baseValue)) { - setErrorText(errorTextFloating()); - return false; - } return true; }, - [ - minValue, - maxValue, - valueToBaseUnit, - errorTextNatural, - errorTextBounds, - errorTextFloating, - ] + [minValue, maxValue, valueToBaseUnit, errorTextNatural, errorTextBounds] ); /* applies the selected unit and returns the base-unit value */ @@ -116,9 +108,17 @@ const UnitInputField = ({ setValidated('default'); } else if (isValid(inputValue)) { const baseValue = valueToBaseUnit(inputValue); - onChange(baseValue); + let validatedValue = baseValue; + if (baseValue !== Math.floor(baseValue)) { + validatedValue = Math.floor(baseValue); + setErrorText(warningTextRounded(validatedValue)); + setValidated('warning'); + } else { + // Keep baseValue as validatedValue + setValidated('default'); + } + onChange(validatedValue); handleInputValidation(true); - setValidated('default'); } else { handleInputValidation(false); setValidated('error'); @@ -131,6 +131,7 @@ const UnitInputField = ({ onChange, isValid, valueToBaseUnit, + warningTextRounded, ]); /* set the selected unit */ @@ -181,6 +182,7 @@ const UnitInputField = ({ label={__('Quota Limit')} validated={validated} helperTextInvalid={errorText} + helperText={helperTextWarning(errorText, validated !== 'warning')} fieldId="quota-limit-resource-quota-form-group" labelIcon={labelIcon || {}} > diff --git a/webpack/components/ResourceQuotaForm/components/Resource/__test__/UnitInputField.test.js b/webpack/components/ResourceQuotaForm/components/Resource/__test__/UnitInputField.test.js new file mode 100644 index 0000000..1f93eb1 --- /dev/null +++ b/webpack/components/ResourceQuotaForm/components/Resource/__test__/UnitInputField.test.js @@ -0,0 +1,108 @@ +import React from 'react'; +import '@testing-library/jest-dom'; +import { render, screen, fireEvent } from '@testing-library/react'; + +import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; +import LabelIcon from 'foremanReact/components/common/LabelIcon'; + +import UnitInputField from '../UnitInputField'; + +const getDefaultProps = () => ({ + initialValue: 0, + onChange: jest.fn(), + isDisabled: false, + handleInputValidation: jest.fn(), + units: [ + { symbol: 'MiB', factor: 1 }, + { symbol: 'GiB', factor: 1024 }, + ], + labelIcon: , + minValue: 0, + maxValue: 5, +}); + +const fixtureDefault = { + 'should render default': { + ...getDefaultProps(), + }, +}; + +const fixtureSingleUnit = { + 'should render without dropdown (single unit)': { + ...getDefaultProps(), + units: [{ symbol: 'cores', factor: 1 }], + }, +}; + +const fixtureDisabled = { + 'should render as disabled field': { + ...getDefaultProps(), + isDisabled: true, + }, +}; + +describe('UnitInputField', () => { + testComponentSnapshotsWithFixtures(UnitInputField, fixtureDefault); + testComponentSnapshotsWithFixtures(UnitInputField, fixtureSingleUnit); + testComponentSnapshotsWithFixtures(UnitInputField, fixtureDisabled); + + it('triggers handleInputValidation on unit change', async () => { + const props = getDefaultProps(); + + render(); + const input = screen.getByRole('textbox'); + fireEvent.change(input, { target: { value: 3 } }); + + // gets called (1.) with initialValue and (2.) the simulated change + expect(props.onChange).toHaveBeenCalledTimes(2); + expect(props.onChange).toHaveBeenCalledWith(props.initialValue); + expect(props.onChange).toHaveBeenLastCalledWith(3); + expect(props.handleInputValidation).toHaveBeenCalledTimes(2); + expect(props.handleInputValidation).toHaveBeenCalledWith(true); + }); + + test('triggers onChange with rounded value', () => { + const props = getDefaultProps(); + + render(); + const input = screen.getByRole('textbox'); + fireEvent.change(input, { target: { value: 3.5 } }); + + // gets called (1.) with initialValue and (2.) the simulated change + expect(props.onChange).toHaveBeenCalledTimes(2); + expect(props.onChange).toHaveBeenCalledWith(props.initialValue); + expect(props.onChange).toHaveBeenLastCalledWith(3); + expect(props.handleInputValidation).toHaveBeenCalledTimes(2); + expect(props.handleInputValidation).toHaveBeenCalledWith(true); + }); + + test('does not trigger onChange when value out of bounds', () => { + const props = getDefaultProps(); + + render(); + const input = screen.getByRole('textbox'); + fireEvent.change(input, { target: { value: props.maxValue + 1 } }); + + // onChange only called for initialValue + expect(props.onChange).toHaveBeenCalledTimes(1); + expect(props.onChange).toHaveBeenCalledWith(props.initialValue); + // handleInputValidation called with false => invalid + expect(props.handleInputValidation).toHaveBeenCalledTimes(2); + expect(props.handleInputValidation).toHaveBeenLastCalledWith(false); + }); + + test('does not trigger onChange when value is not a number', () => { + const props = getDefaultProps(); + + render(); + const input = screen.getByRole('textbox'); + fireEvent.change(input, { target: { value: 'no number' } }); + + // onChange only called for initialValue + expect(props.onChange).toHaveBeenCalledTimes(1); + expect(props.onChange).toHaveBeenCalledWith(props.initialValue); + // handleInputValidation called with false => invalid + expect(props.handleInputValidation).toHaveBeenCalledTimes(2); + expect(props.handleInputValidation).toHaveBeenLastCalledWith(false); + }); +}); diff --git a/webpack/components/ResourceQuotaForm/components/Resource/__test__/__snapshots__/UnitInputField.test.js.snap b/webpack/components/ResourceQuotaForm/components/Resource/__test__/__snapshots__/UnitInputField.test.js.snap new file mode 100644 index 0000000..4bbacc3 --- /dev/null +++ b/webpack/components/ResourceQuotaForm/components/Resource/__test__/__snapshots__/UnitInputField.test.js.snap @@ -0,0 +1,155 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UnitInputField should render as disabled field 1`] = ` + + + + } + helperTextInvalid="" + label="Quota Limit" + labelIcon={ + + } + validated="default" +> + + + + MiB + , + + GiB + , + ] + } + isOpen={false} + onSelect={[Function]} + toggle={ + + MiB + + } + /> + + +`; + +exports[`UnitInputField should render default 1`] = ` + + + + } + helperTextInvalid="" + label="Quota Limit" + labelIcon={ + + } + validated="default" +> + + + + MiB + , + + GiB + , + ] + } + isOpen={false} + onSelect={[Function]} + toggle={ + + MiB + + } + /> + + +`; + +exports[`UnitInputField should render without dropdown (single unit) 1`] = ` + + + + } + helperTextInvalid="" + label="Quota Limit" + labelIcon={ + + } + validated="default" +> + + + + cores + + + +`;