diff --git a/src/__internal__/fieldset/fieldset.component.tsx b/src/__internal__/fieldset/fieldset.component.tsx index 589fccc8b0..86b62c4fed 100644 --- a/src/__internal__/fieldset/fieldset.component.tsx +++ b/src/__internal__/fieldset/fieldset.component.tsx @@ -107,6 +107,7 @@ const Fieldset = ({ rightPadding={legendSpacing} {...legendMargin} data-element="legend" + data-role="legend" > { {(error || warning) && ( - + )} ) { - const children = buttonValues.map((value, index) => ( - - )); - - return mount( - - {children} - - ); -} - -function renderRadioButtonGroupWithNewValidation({ - name = "test-validation-group", - ...props -}: Partial) { - const children = buttonValues.map((value, index) => ( - - )); - - return mount( - - - {children} - - - ); -} - -describe("RadioButtonGroup", () => { - beforeAll(() => { - loggerSpy.mockImplementation(() => {}); - }); - - afterAll(() => { - loggerSpy.mockRestore(); - }); - - describe("with an inline legend", () => { - describe("when adaptiveLegendBreakpoint prop is set", () => { - describe("when screen bigger than breakpoint", () => { - beforeEach(() => { - mockMatchMedia(true); - }); - - it("should pass legendInline to Fieldset", () => { - const wrapper = renderRadioButtonGroup({ - legend: "Legend", - legendInline: true, - adaptiveLegendBreakpoint: 1000, - }); - - expect(wrapper.find(Fieldset).props().inline).toEqual(true); - }); - }); - - describe("when screen smaller than breakpoint", () => { - beforeEach(() => { - mockMatchMedia(false); - }); - - it("should pass legendInline to Fieldset", () => { - const wrapper = renderRadioButtonGroup({ - legend: "Legend", - legendInline: true, - adaptiveLegendBreakpoint: 1000, - }); - - expect(wrapper.find(Fieldset).props().inline).toEqual(false); - }); - }); - }); - }); - - describe("with a left margin (ml prop)", () => { - describe("when adaptiveSpacingBreakpoint prop is set", () => { - describe("when screen bigger than breakpoint", () => { - beforeEach(() => { - mockMatchMedia(true); - }); - - it("should pass the correct margin to Fieldset", () => { - const wrapper = renderRadioButtonGroup({ - legend: "Legend", - legendInline: true, - adaptiveSpacingBreakpoint: 1000, - ml: "10%", - }); - - expect(wrapper.find(Fieldset).props().ml).toEqual("10%"); - }); - }); - - describe("when screen smaller than breakpoint", () => { - beforeEach(() => { - mockMatchMedia(false); - }); - - it('should pass "0" to Fieldset', () => { - const wrapper = renderRadioButtonGroup({ - legend: "Legend", - legendInline: true, - adaptiveSpacingBreakpoint: 1000, - ml: "10%", - }); - - expect(wrapper.find(Fieldset).props().ml).toEqual(undefined); - }); - }); - }); - }); - - describe("styles", () => { - it("applies the correct Legend Container styles", () => { - assertStyleMatch( - { - display: "flex", - }, - mount() - ); - }); - }); - - describe("validations", () => { - it.each(["error", "warning", "info"])( - "when %s is passed as %s it is passed as boolean to RadioButton", - (type) => { - const wrapper = renderRadioButtonGroup({ [type]: true }); - wrapper - .find(RadioButton) - .forEach((node) => - expect(node.props()[type as "error" | "warning" | "info"]).toBe( - true - ) - ); - } - ); - - it("blocks the group behaviour if no validation set on group", () => { - const wrapper = renderRadioButtonGroup({}); - expect(wrapper.find(Fieldset).props().blockGroupBehaviour).toEqual(true); - }); - }); - - describe("required", () => { - let wrapper: ReactWrapper; - - beforeAll(() => { - wrapper = mount( - {}} - legend="Group Label" - required - > - - - - ); - }); - - it("the attribute is set on the inputs when prop is true", () => { - const inputs = wrapper.find("input"); - inputs.forEach((input) => { - expect(input.getDOMNode()).toHaveAttribute("required", ""); - }); - }); - - it("the isRequired prop is not passed to the labels", () => { - const labels = wrapper.find(Label); - labels.forEach((label) => { - expect(label.prop("isRequired")).toBe(undefined); - }); - }); - - it("the isRequired prop is passed to the fieldset", () => { - const fieldset = wrapper.find(Fieldset); - expect(fieldset.prop("isRequired")).toBe(true); - }); - }); - - it("should append the expected text to the legend element when the isOptional prop is true", () => { - assertStyleMatch( - { - content: '"(optional)"', - }, - mount( - {}} - legend="Group Label" - isOptional - > - - - - ).find(StyledLegendContent), - { modifier: "::after" } - ); - }); - - describe("tooltipPosition", () => { - it("overrides the default position when value is passed", () => { - const { position } = renderRadioButtonGroup({ - error: "message", - tooltipPosition: "bottom", - }) - .find(Tooltip) - .props(); - - expect(position).toEqual("bottom"); - }); - }); - - describe("when children are passed in an array", () => { - it("should render the list correctly", () => { - const radioGroup = mount( - - {[ - , - null, - undefined, - "foo", - , - ]} - - ); - - expect(radioGroup.find(RadioButton).at(0).props().checked).toBe(true); - expect(radioGroup.find(RadioButton).at(1).props().checked).toBe(false); - }); - }); - - describe("New Validations", () => { - let wrapper; - it("should apply the correct styles for error", () => { - wrapper = renderRadioButtonGroupWithNewValidation({ error: "message" }); - assertStyleMatch( - { - position: "absolute", - zIndex: "6", - width: "2px", - backgroundColor: "var(--colorsSemanticNegative500)", - left: "-12px", - bottom: "0px", - top: "0px", - }, - wrapper.find(ErrorBorder) - ); - }); - - it("should apply the correct styles for warning", () => { - wrapper = renderRadioButtonGroupWithNewValidation({ warning: "message" }); - assertStyleMatch( - { - position: "absolute", - zIndex: "6", - width: "2px", - backgroundColor: "var(--colorsSemanticCaution500)", - left: "-12px", - bottom: "0px", - top: "0px", - }, - wrapper.find(ErrorBorder) - ); - }); - - it("should apply the correct styles for error border when inline is true", () => { - wrapper = renderRadioButtonGroupWithNewValidation({ - error: "message", - inline: true, - }); - assertStyleMatch( - { - position: "absolute", - zIndex: "6", - width: "2px", - backgroundColor: "var(--colorsSemanticNegative500)", - left: "-12px", - bottom: "10px", - top: "0px", - }, - wrapper.find(ErrorBorder) - ); - }); - - it("should apply the correct styles for legend help", () => { - wrapper = renderRadioButtonGroupWithNewValidation({ - error: "message", - legend: "Label", - legendHelp: "Hint Text", - }); - assertStyleMatch( - { - marginTop: "-4px", - marginBottom: "8px", - color: "var(--colorsUtilityYin055)", - fontSize: "14px", - }, - wrapper.find(StyledHintText) - ); - }); - - it("when children are passed in an array, component should render correctly", () => { - wrapper = mount( - - {}} - > - {[ - , - null, - undefined, - "foo", - , - ]} - - - ); - expect(wrapper.find(RadioButton).at(0).exists()).toBe(true); - }); - }); -}); diff --git a/src/components/radio-button/radio-button-group/radio-button-group.test.tsx b/src/components/radio-button/radio-button-group/radio-button-group.test.tsx new file mode 100644 index 0000000000..48a4045068 --- /dev/null +++ b/src/components/radio-button/radio-button-group/radio-button-group.test.tsx @@ -0,0 +1,299 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { RadioButton, RadioButtonGroup } from ".."; +import CarbonProvider from "../../carbon-provider"; +import Logger from "../../../__internal__/utils/logger"; +import { mockMatchMedia } from "../../../__spec_helper__/__internal__/test-utils"; + +test("logs a deprecation warning for uncontrolled behaviour", () => { + const loggerSpy = jest + .spyOn(Logger, "deprecate") + .mockImplementation(() => {}); + render( + + + + ); + + expect(loggerSpy).toHaveBeenCalledWith( + "Uncontrolled behaviour in `Radio Button` is deprecated and support will soon be removed. Please make sure all your inputs are controlled." + ); + expect(loggerSpy).toHaveBeenCalledTimes(1); + + loggerSpy.mockRestore(); +}); + +test("renders with provided `RadioButton` children", () => { + render( + {}}> + + + ); + + const radioButton = screen.getByRole("radio", { name: "Radio Button 1" }); + + expect(radioButton).toBeInTheDocument(); +}); + +test("renders with RadioButton and non-`RadioButton` children when passed as an array", () => { + render( + {}}> + {[ + , + null, + undefined, + "foo", + , + ]} + + ); + + expect(screen.getAllByRole("radio")).toHaveLength(2); + expect(screen.getByText("foo")).toBeVisible(); +}); + +test("renders fieldset with provided `legend`", () => { + render( + {}} + > + + + ); + + const fieldset = screen.getByRole("group", { name: "Radio Group Legend" }); + + expect(fieldset).toBeVisible(); +}); + +test("checks the correct `RadioButton` child when a value is passed", () => { + render( + {}}> + + + + ); + + const radio1 = screen.getByRole("radio", { name: "Radio Button 1" }); + const radio2 = screen.getByRole("radio", { name: "Radio Button 2" }); + + expect(radio1).not.toBeChecked(); + expect(radio2).toBeChecked(); +}); + +test("calls `onChange` when a `RadioButton` child is checked", async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + render( + + + + ); + + const radioButton = screen.getByRole("radio"); + + await user.click(radioButton); + + expect(onChange).toHaveBeenCalled(); +}); + +test("calls `onBlur` when a `RadioButton` child is blurred", async () => { + const user = userEvent.setup(); + const onBlur = jest.fn(); + render( + {}} onBlur={onBlur}> + + + ); + + await user.tab(); + await user.tab(); + + expect(onBlur).toHaveBeenCalled(); +}); + +test("renders required `RadioButton` children when `required` prop is true", () => { + render( + {}}> + + + ); + + const radioButton = screen.getByRole("radio", { name: "Radio Button 1" }); + + expect(radioButton).toBeRequired(); +}); + +test("renders with inline legend when screen is larger than `adaptiveLegendBreakpoint`", () => { + mockMatchMedia(true); + render( + {}} + > + + + ); + + const legend = screen.getByTestId("legend"); + + expect(legend).toHaveStyle({ float: "left" }); +}); + +test("renders with legend on top when screen is smaller than `adaptiveLegendBreakpoint`", () => { + mockMatchMedia(false); + render( + {}} + > + + + ); + + const legend = screen.getByTestId("legend"); + + expect(legend).not.toHaveStyle({ float: "left" }); +}); + +test("renders with provided margin-left when screen is larger than `adaptiveSpacingBreakpoint`", () => { + mockMatchMedia(true); + render( + {}} + > + + + ); + + const fieldset = screen.getByRole("group"); + + expect(fieldset).toHaveStyle({ marginLeft: "20%" }); +}); + +test("does not render with provided margin-left when screen is smaller than `adaptiveSpacingBreakpoint`", () => { + mockMatchMedia(false); + render( + {}} + > + + + ); + + const fieldset = screen.getByRole("group"); + + expect(fieldset).toHaveStyle({ marginLeft: "0" }); +}); + +// coverage +describe("when `validationRedesignOptIn` flag is true", () => { + it("renders with provided `error`", () => { + render( + + {}} + > + + + + ); + + expect(screen.getByText("Error message")).toBeVisible(); + }); + + it("renders with provided `warning`", () => { + render( + + {}} + > + + + + ); + + expect(screen.getByText("Warning message")).toBeVisible(); + }); + + it("renders `legendHelp` as hint text", () => { + render( + + {}} + > + + + + ); + + const hintText = screen.getByText("Hint message"); + + expect(hintText).toBeVisible(); + expect(hintText).toHaveStyle({ + marginTop: "-4px", + marginBottom: "8px", + color: "var(--colorsUtilityYin055)", + fontSize: "14px", + }); + }); + + it("renders ErrorBorder with expected styles when `inline` is true", () => { + render( + + {}} + > + + + + ); + + const errorBorder = screen.getByTestId("radio-error-border"); + + expect(errorBorder).toHaveStyle({ + bottom: "10px", + }); + }); + + it("renders with RadioButton and non-`RadioButton` children when passed as an array", () => { + render( + + {}}> + {[ + , + null, + undefined, + "foo", + , + ]} + + + ); + + expect(screen.getAllByRole("radio")).toHaveLength(2); + expect(screen.getByText("foo")).toBeVisible(); + }); +}); diff --git a/src/components/radio-button/radio-button-svg.component.tsx b/src/components/radio-button/radio-button-svg.component.tsx index 73c3a62515..e0002c9d4a 100644 --- a/src/components/radio-button/radio-button-svg.component.tsx +++ b/src/components/radio-button/radio-button-svg.component.tsx @@ -4,7 +4,7 @@ import StyledCheckableInputSvgWrapper from "../../__internal__/checkable-input/c const RadioButtonSvg = () => { return ( - + ).mockReturnValue(mockedGuid); - -function renderRadioButton( - buttonProps = {}, - validations = {}, - theme = sageTheme, - renderer = mount -) { - return renderer( - - - - - - ); -} - -const getRadioButton = (wrapper: ReactWrapper) => wrapper.find(RadioButton); - -const validationTypes: ("error" | "warning" | "info")[] = [ - "error", - "warning", - "info", -]; -const borderColorsByValidationTypes = { - error: "var(--colorsSemanticNegative500)", - warning: "var(--colorsSemanticCaution500)", - info: "var(--colorsSemanticInfo500)", -}; - -describe("RadioButton", () => { - let loggerSpy: jest.SpyInstance | jest.Mock; - - beforeEach(() => { - loggerSpy = jest.spyOn(Logger, "deprecate"); - }); - - afterEach(() => { - loggerSpy.mockRestore(); - }); - - afterAll(() => { - loggerSpy.mockClear(); - }); - - describe("Deprecation warning for uncontrolled", () => { - it("should display deprecation warning once", () => { - mount( - - - - ); - - expect(loggerSpy).toHaveBeenCalledWith( - "Uncontrolled behaviour in `Radio Button` is deprecated and support will soon be removed. Please make sure all your inputs are controlled." - ); - - expect(loggerSpy).toHaveBeenCalledTimes(1); - }); - }); - - describe("propTypes", () => { - it("does not allow a children prop", () => { - const consoleSpy = jest - .spyOn(console, "error") - .mockImplementation(() => {}); - const expected = - "This component is meant to be used as a self-closing tag. " + - "You should probably use the label prop instead."; - - expect(() => renderRadioButton({ children: "someChildren" })).toThrow( - expected - ); - - consoleSpy.mockRestore(); - }); - }); - - it("the input ref should be forwarded", () => { - let mockRef: React.RefObject | undefined; - - const WrapperComponent = () => { - mockRef = useRef(null); - - return ; - }; - - const wrapper = mount(); - - expect(mockRef?.current).toBe( - wrapper.find(HiddenCheckableInputStyle).getDOMNode() - ); - }); - - describe("when disabled === true", () => { - describe("default", () => { - const wrapper = renderRadioButton({ disabled: true }); - - it("disables the input", () => { - const radioInput = wrapper.find("input"); - expect(radioInput.getDOMNode().disabled).toBe(true); - }); - - it("applies the correct circle styles", () => { - assertStyleMatch( - { fill: "var(--colorsUtilityDisabled400)" }, - getRadioButton(wrapper), - { modifier: "circle" } - ); - }); - - it("renders the correct checked colour", () => { - assertStyleMatch( - { fill: "var(--colorsUtilityDisabled600)" }, - getRadioButton(wrapper), - { - modifier: `${`${HiddenCheckableInputStyle}:checked + ${StyledCheckableInputSvgWrapper} circle`}`, - } - ); - }); - }); - }); - - describe('when size === "large"', () => { - describe("default", () => { - const wrapper = getRadioButton(renderRadioButton({ size: "large" })); - const dimensions = { height: "24px", width: "24px" }; - - it("applies the correct input styles", () => { - assertStyleMatch({ ...dimensions }, wrapper, { - modifier: `${StyledCheckableInput}`, - }); - }); - - it("applies the correct hidden input styles", () => { - assertStyleMatch(dimensions, wrapper, { - modifier: `${HiddenCheckableInputStyle}`, - }); - }); - - it("applies the correct svg wrapper styles", () => { - assertStyleMatch(dimensions, wrapper, { - modifier: `${StyledCheckableInputSvgWrapper}`, - }); - }); - - it("applies the correct svg styles", () => { - assertStyleMatch(dimensions, wrapper, { modifier: "svg" }); - }); - - it("applies the correct circle styles", () => { - assertStyleMatch({ r: "3.75" }, wrapper, { modifier: "circle" }); - }); - }); - - describe("and reverse === true", () => { - describe("default", () => { - const wrapper = getRadioButton( - renderRadioButton({ reverse: true, size: "large" }) - ); - - it("applies the correct FieldHelp styles", () => { - assertStyleMatch({ padding: "0" }, wrapper, { - modifier: `${FieldHelpStyle}`, - }); - }); - }); - - describe("and fieldHelpInline === true", () => { - it("does not apply padding changes to FieldHelp", () => { - const wrapper = renderRadioButton({ - fieldHelpInline: true, - reverse: true, - size: "large", - }); - assertStyleMatch({ padding: undefined }, getRadioButton(wrapper), { - modifier: `${FieldHelpStyle}`, - }); - }); - }); - }); - }); - - describe("validations", () => { - describe.each(validationTypes)("%s === true", (type) => { - it("show correct border on radio", () => { - const wrapper = renderRadioButton({}, { [type]: true }); - const borderWidth = type === "error" ? 2 : 1; - assertStyleMatch( - { - border: `${borderWidth}px solid ${borderColorsByValidationTypes[type]}`, - }, - wrapper.find(RadioButton).at(0), - { modifier: "svg" } - ); - }); - }); - - describe.each(validationTypes)('%s === "string"', (type) => { - it("show correct border on radio", () => { - const wrapper = renderRadioButton({}, { [type]: "Message" }); - const borderWidth = type === "error" ? 2 : 1; - assertStyleMatch( - { - border: `${borderWidth}px solid ${borderColorsByValidationTypes[type]}`, - }, - wrapper.find(RadioButton).at(0), - { modifier: "svg" } - ); - }); - }); - }); - - describe("tooltipPosition", () => { - it("overrides the default position when value is passed", () => { - const { position } = mount( - - ) - .find(Tooltip) - .props(); - - expect(position).toEqual("bottom"); - }); - }); - - it("applies the correct Legend Container styles", () => { - assertStyleMatch( - { - marginLeft: "32px", - }, - mount(), - { modifier: "&:not(:first-of-type)" } - ); - }); - - describe("helpAriaLabel", () => { - it("should set the aria-label on the Help component", () => { - const text = "foo"; - - const { "aria-label": ariaLabel } = mount( - - ) - .find(StyledHelp) - .props(); - - expect(ariaLabel).toEqual(text); - }); - }); - - it("has the expected border radius styling", () => { - const wrapper = mount(); - - assertStyleMatch( - { - borderRadius: "var(--borderRadiusCircle)", - }, - wrapper, - { modifier: `${StyledCheckableInputSvgWrapper}` } - ); - - assertStyleMatch( - { - borderRadius: "var(--borderRadiusCircle)", - }, - wrapper, - { modifier: "svg" } - ); - }); -}); diff --git a/src/components/radio-button/radio-button.test.tsx b/src/components/radio-button/radio-button.test.tsx new file mode 100644 index 0000000000..ce61c02429 --- /dev/null +++ b/src/components/radio-button/radio-button.test.tsx @@ -0,0 +1,226 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import RadioButton from "."; + +test("renders with provided `label`", () => { + render(); + + const radioButton = screen.getByRole("radio", { name: "Radio Button 1" }); + + expect(radioButton).toBeInTheDocument(); + expect(screen.getByTestId("radio-svg")).toBeVisible(); + expect(screen.getByText("Radio Button 1")).toBeVisible(); +}); + +test("calls `onClick` when provided and the radio button is clicked", async () => { + const user = userEvent.setup(); + const onClick = jest.fn(); + render( + + ); + + const radioButton = screen.getByRole("radio"); + + await user.click(radioButton); + + expect(onClick).toHaveBeenCalledTimes(1); +}); + +test("calls `onChange` when provided and the radio button is clicked", async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + render( + + ); + + const radioButton = screen.getByRole("radio"); + + await user.click(radioButton); + + expect(onChange).toHaveBeenCalledTimes(1); +}); + +test("calls `onFocus` when provided and the radio button is focused", async () => { + const user = userEvent.setup(); + const onFocus = jest.fn(); + render( + + ); + + const radioButton = screen.getByRole("radio", { name: "Radio Button 1" }); + await user.click(radioButton); + + expect(onFocus).toHaveBeenCalledTimes(1); +}); + +test("calls `onBlur` when provided and the radio button is blurred", async () => { + const user = userEvent.setup(); + const onBlur = jest.fn(); + render(); + + const radioButton = screen.getByRole("radio", { name: "Radio Button 1" }); + await user.click(radioButton); + await user.tab(); + + expect(onBlur).toHaveBeenCalledTimes(1); +}); + +test("calls `onMouseEnter` when provided and the radio button is hovered", async () => { + const user = userEvent.setup(); + const onMouseEnter = jest.fn(); + render( + + ); + + const radioButton = screen.getByRole("radio"); + + await user.hover(radioButton); + + expect(onMouseEnter).toHaveBeenCalledTimes(1); +}); + +test("calls `onMouseLeave` when provided and the radio button is no longer hovered", async () => { + const user = userEvent.setup(); + const onMouseLeave = jest.fn(); + render( + + ); + + const radioButton = screen.getByRole("radio"); + + await user.hover(radioButton); + await user.unhover(radioButton); + + expect(onMouseLeave).toHaveBeenCalledTimes(1); +}); + +test("forwards the provided ref to the input", () => { + const ref = React.createRef(); + render(); + + const radioButton = screen.getByRole("radio", { name: "Radio Button 1" }); + + expect(ref.current).toBe(radioButton); +}); + +test("throws an error if children are passed", () => { + const consoleError = jest + .spyOn(console, "error") + .mockImplementation(() => {}); + + expect(() => { + render( + +
child
+
+ ); + }).toThrow( + "This component is meant to be used as a self-closing tag. " + + "You should probably use the label prop instead." + ); + + consoleError.mockRestore(); +}); + +test("renders disabled when `disabled` prop is true", () => { + render(); + + const radioButton = screen.getByRole("radio"); + + expect(radioButton).toBeDisabled(); +}); + +test("renders checked when `checked` prop is true", () => { + render(); + + const radioButton = screen.getByRole("radio"); + + expect(radioButton).toBeChecked(); +}); + +test("renders with a help tooltip if `labelHelp` is provided", async () => { + const user = userEvent.setup(); + render( + + ); + + const helpIcon = screen.getByRole("button", { name: "help" }); + await user.hover(helpIcon); + + const helpTooltip = screen.getByRole("tooltip", { name: "labelHelp" }); + + expect(helpTooltip).toBeVisible(); +}); + +test("sets the aria-label of the help icon to the provided `helpAriaLabel`", () => { + render( + + ); + + const helpIcon = screen.getByRole("button", { name: "helpAriaLabel" }); + + expect(helpIcon).toBeVisible(); +}); + +// coverage +test("renders with expected styles when `size` is 'large'", () => { + render(); + + const radioButton = screen.getByRole("radio"); + + expect(radioButton).toHaveStyle({ width: "24px", height: "24px" }); + expect(screen.getByTestId("radio-svg")).toHaveStyle({ + width: "24px", + height: "24px", + }); +}); + +// coverage +test("renders with expected styles when `reverse` prop is true", () => { + render( + + ); + + expect(screen.getByText("fieldHelp")).toHaveStyle({ + marginLeft: "0px", + marginRight: "6px", + }); + + expect(screen.getByText("Radio Button 1")).toHaveStyle({ flex: "0 1 auto" }); +}); + +// coverage +test("renders `fieldHelp` with expected styles when `size` is 'large' and `reverse` is true", () => { + render( + + ); + + expect(screen.getByText("fieldHelp")).toHaveStyle({ + padding: "0", + }); +});