diff --git a/src/components/tabs/__internal__/tab-title/tab-title.component.tsx b/src/components/tabs/__internal__/tab-title/tab-title.component.tsx index 3822909e46..2bb97a664c 100644 --- a/src/components/tabs/__internal__/tab-title/tab-title.component.tsx +++ b/src/components/tabs/__internal__/tab-title/tab-title.component.tsx @@ -175,6 +175,7 @@ const TabTitle = React.forwardRef( const titleChildren = ( <> )} diff --git a/src/components/tabs/__internal__/tab-title/tab-title.spec.tsx b/src/components/tabs/__internal__/tab-title/tab-title.spec.tsx deleted file mode 100644 index 4ae7caf6de..0000000000 --- a/src/components/tabs/__internal__/tab-title/tab-title.spec.tsx +++ /dev/null @@ -1,1417 +0,0 @@ -import React from "react"; -import { mount, ReactWrapper, shallow, ShallowWrapper } from "enzyme"; -import { act } from "react-dom/test-utils"; -import TabTitle, { TabTitleProps } from "./tab-title.component"; -import { - StyledTabTitleButton, - StyledTabTitleLink, - StyledTitleContent, - StyledLayoutWrapper, - StyledSelectedIndicator, -} from "./tab-title.style"; -import { sageTheme } from "../../../../style/themes"; -import { assertStyleMatch } from "../../../../__spec_helper__/__internal__/test-utils"; -import ValidationIcon from "../../../../__internal__/validations/validation-icon.component"; -import StyledValidationIcon from "../../../../__internal__/validations/validation-icon.style"; -import Icon from "../../../icon"; -import StyledIcon from "../../../icon/icon.style"; -import Tooltip from "../../../tooltip"; -import { ThemeObject } from "../../../../style/themes/base"; - -function render( - props: Partial & { - theme?: Partial; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ref?: React.ForwardedRef; - } = {}, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - renderer: any = shallow, - container = {} -) { - return renderer( - {}} - onKeyDown={() => {}} - title="Tab Title 1" - dataTabId="uniqueid1" - {...props} - />, - container - ); -} - -const TEST_SIBLINGS = [foo, bar]; - -describe("TabTitle", () => { - let wrapper: ReactWrapper | ShallowWrapper; - - let globalOpenMock: jest.SpyInstance; - - beforeEach(() => { - globalOpenMock = jest.spyOn(global, "open").mockImplementation(() => null); - }); - - afterEach(() => { - globalOpenMock.mockClear(); - }); - - it("renders as expected", () => { - assertStyleMatch( - { - backgroundColor: "transparent", - display: "inline-block", - fontWeight: "500", - height: "var(--sizing500)", - }, - render({}, mount).find(StyledTabTitleButton) - ); - }); - - it("renders a title as its child with a text passed as a prop", () => { - const firstTabTitle = "Tab Title 1"; - wrapper = render(); - expect(wrapper.children()).toHaveLength(1); - expect(wrapper.children().text()).toEqual(firstTabTitle); - }); - - it("contains custom className if passed as a prop", () => { - wrapper = render({ className: "class" }); - expect(wrapper.find(".class").exists()).toEqual(true); - }); - - describe("attributes", () => { - it('role equals "tab"', () => { - wrapper = render(); - expect(wrapper.find("[role='tab']").exists()).toEqual(true); - }); - it('data-element equals "select-tab"', () => { - wrapper = render(); - expect(wrapper.find("[data-element='select-tab']").exists()).toEqual( - true - ); - }); - it("data-tabid equals tabId", () => { - wrapper = render(); - expect(wrapper.find("[data-tabid='uniqueid1']").exists()).toEqual(true); - }); - it("when 'data-role' is set, pass to title element", () => { - wrapper = render({ "data-role": "foobar" }); - expect(wrapper.find("[data-role='foobar']").exists()).toBeTruthy(); - }); - }); - - describe('when size is set to "large"', () => { - it("applies proper styling", () => { - wrapper = render({ size: "large" }, mount); - assertStyleMatch( - { padding: "14px 24px" }, - wrapper.find(StyledTitleContent) - ); - assertStyleMatch( - { height: "var(--sizing600)" }, - wrapper.find(StyledTabTitleButton) - ); - }); - }); - - describe("when `href` provided", () => { - it("should trigger open in new tab if pressed with Enter or Space", () => { - wrapper = render({ href: "randomUrl" }); - - wrapper - .find(StyledTabTitleLink) - .props() - .onKeyDown({ key: " ", stopPropagation: () => {} }); - expect(globalOpenMock).toHaveBeenCalledWith("randomUrl", "_blank"); - }); - - it("should trigger open in new tab if clicked", () => { - wrapper = mount( - {}} - onKeyDown={() => {}} - > - - - ); - - wrapper - .find(StyledTabTitleLink) - .props() - .onClick({ stopPropagation: () => {}, preventDefault: () => {} }); - expect(globalOpenMock).toHaveBeenCalledWith("randomUrl", "_blank"); - }); - }); - - describe("when `href` is not provided", () => { - it("should not trigger open in new tab if pressed with Enter or Space", () => { - wrapper = render({ onKeyDown: () => {} }); - - wrapper - .find(StyledTabTitleButton) - .props() - .onKeyDown({ key: " ", stopPropagation: () => {} }); - expect(globalOpenMock).not.toHaveBeenCalled(); - }); - }); - - describe('when position is "top', () => { - describe("with borders", () => { - it("applies proper styling", () => { - wrapper = render({ size: "large", borders: true }, mount); - - assertStyleMatch( - { - borderTop: "1px solid var(--colorsActionMinor100)", - borderLeft: "1px solid var(--colorsActionMinor100)", - borderRight: "1px solid var(--colorsActionMinor100)", - }, - wrapper.find(StyledTitleContent) - ); - - assertStyleMatch( - { - marginLeft: "-1px", - }, - wrapper.find(StyledTabTitleButton), - { modifier: ":nth-of-type(n + 1):not(:first-of-type)" } - ); - }); - - it('applies proper styling when size is "large" and isTabSelected is true', () => { - wrapper = render( - { size: "large", borders: true, isTabSelected: true }, - mount - ); - - assertStyleMatch( - { paddingBottom: "9px" }, - wrapper.find(StyledTitleContent) - ); - - assertStyleMatch( - { - bottom: "0px", - left: "0px", - right: "0px", - boxShadow: - "inset 0px calc(-1 * var(--sizing050)) 0px var(--colorsActionMajor500)", - }, - wrapper.find(StyledSelectedIndicator) - ); - }); - - it('applies proper styling when size is not "large" and isTabSelected is true', () => { - wrapper = render( - { borders: true, isTabSelected: true, size: "default" }, - mount - ); - - assertStyleMatch( - { padding: "10px 16px" }, - wrapper.find(StyledTitleContent) - ); - - assertStyleMatch( - { - bottom: "0px", - left: "0px", - boxShadow: - "inset 0px calc(-1 * var(--sizing050)) 0px var(--colorsActionMajor500)", - height: "var(--sizing050)", - }, - wrapper.find(StyledSelectedIndicator) - ); - }); - }); - }); - - describe('when position prop is set to "left"', () => { - it("applies proper styles", () => { - wrapper = render({ position: "left" }, mount); - assertStyleMatch( - { - backgroundColor: "transparent", - borderBottom: "0px", - borderRight: "2px solid var(--colorsActionMinor100)", - display: "flex", - height: "auto", - marginLeft: "0px", - }, - wrapper.find(StyledTabTitleButton) - ); - - assertStyleMatch( - { - background: "var(--colorsActionMinor100)", - }, - wrapper.find(StyledTabTitleButton), - { modifier: ":hover" } - ); - - assertStyleMatch( - { - marginTop: "0", - }, - wrapper.find(StyledTabTitleButton), - { modifier: ":first-child" } - ); - }); - - it("renders as expected when `align='left'`", () => { - assertStyleMatch( - { justifyContent: "flex-start", textAlign: "left" }, - render({ position: "left", align: "left" }, mount).find( - StyledTitleContent - ) - ); - }); - - it("renders as expected when `align='right'`", () => { - assertStyleMatch( - { justifyContent: "flex-end", textAlign: "right" }, - render({ align: "right", position: "left" }, mount).find( - StyledTitleContent - ) - ); - }); - - it('applies proper styling when size is "large"', () => { - wrapper = render({ position: "left", size: "large" }, mount); - - assertStyleMatch( - { - padding: "14px 24px", - }, - wrapper.find(StyledTitleContent) - ); - }); - - describe("with borders", () => { - it.each(["default", "large"])( - "applies proper styling when isTabSelected is %s", - () => { - wrapper = render( - { borders: true, isTabSelected: true, position: "left" }, - mount - ); - - assertStyleMatch( - { - borderTop: "1px solid var(--colorsActionMinor100)", - borderLeft: "1px solid var(--colorsActionMinor100)", - borderBottom: "1px solid var(--colorsActionMinor100)", - }, - wrapper.find(StyledTitleContent) - ); - - assertStyleMatch( - { - marginTop: "-1px", - }, - wrapper.find(StyledTabTitleButton), - { modifier: ":nth-of-type(n + 1):not(:first-of-type)" } - ); - - assertStyleMatch( - { - top: "0px", - right: "0px", - bottom: "0px", - boxShadow: - "inset calc(-1 * var(--sizing050)) 0px 0px 0px var(--colorsActionMajor500)", - }, - wrapper.find(StyledSelectedIndicator) - ); - } - ); - }); - }); - - describe("when tab is selected", () => { - it("has aria-selected attribute set to true", () => { - wrapper = render({ isTabSelected: true }); - expect(wrapper.find("[aria-selected=true]").exists()).toEqual(true); - }); - - it("applies proper styling", () => { - wrapper = render({ isTabSelected: true }, mount); - assertStyleMatch( - { - color: "var(--colorsActionMajorYin090)", - backgroundColor: "var(--colorsActionMajorYang100)", - }, - wrapper.find(StyledTabTitleButton) - ); - - assertStyleMatch( - { - backgroundColor: "var(--colorsActionMajorYang100)", - borderBottomColor: "var(--colorsActionMajor500)", - color: "var(--colorsActionMajorYin090)", - }, - wrapper.find(StyledTabTitleButton), - { modifier: ":hover" } - ); - }); - - it("does not apply selected styling", () => { - wrapper = render({ isTabSelected: true, error: true }, mount); - - expect(wrapper.find(StyledSelectedIndicator).exists()).toBeFalsy(); - }); - - it("does not apply selected styling when it has error or warning when size is large", () => { - wrapper = render( - { isTabSelected: true, error: true, size: "large" }, - mount - ); - - expect(wrapper.find(StyledSelectedIndicator).exists()).toBeFalsy(); - }); - - describe("when position prop is set to left", () => { - it("applies proper styling", () => { - wrapper = render({ position: "left", isTabSelected: true }, mount); - - assertStyleMatch( - { - backgroundColor: "var(--colorsActionMajorYang100)", - }, - wrapper.find(StyledTabTitleButton) - ); - - assertStyleMatch( - { - backgroundColor: "var(--colorsActionMajorYang100)", - }, - wrapper.find(StyledTabTitleButton), - { modifier: ":hover" } - ); - }); - - it("does not apply selected styling when it has error or warning", () => { - wrapper = render( - { isTabSelected: true, error: true, position: "left" }, - mount - ); - - expect(wrapper.find(StyledSelectedIndicator).exists()).toBeFalsy(); - }); - - it("does not apply selected styling when it has error or warning and size is large", () => { - wrapper = render( - { isTabSelected: true, error: true, position: "left", size: "large" }, - mount - ); - - expect(wrapper.find(StyledSelectedIndicator).exists()).toBeFalsy(); - }); - }); - }); - - describe("when tab is not selected", () => { - it("has aria-selected attribute set to false", () => { - wrapper = render({ isTabSelected: false }); - expect(wrapper.find("[aria-selected=false]").exists()).toEqual(true); - }); - - it("applies proper styling", () => { - wrapper = render({ isTabSelected: false }, mount); - assertStyleMatch( - { - color: "var(--colorsActionMinorYin090)", - }, - wrapper.find(StyledTabTitleButton) - ); - - assertStyleMatch( - { - background: "var(--colorsActionMinor100)", - color: "var(--colorsActionMinorYin090)", - outline: "none", - }, - wrapper.find(StyledTabTitleButton), - { modifier: ":hover" } - ); - }); - }); - - describe("when title has siblings", () => { - it('renders them as expected when titlePosition is "before"', () => { - wrapper = render( - { - title: "Tab 1", - siblings: TEST_SIBLINGS, - titlePosition: "before", - }, - mount - ); - - expect(wrapper.find(StyledTitleContent).props().hasSiblings).toEqual( - true - ); - expect( - wrapper.find(StyledTitleContent).props().children[0][0].props.children - ).toEqual("Tab 1"); - assertStyleMatch( - { padding: "10px 16px" }, - wrapper.find(StyledTitleContent) - ); - }); - - it('renders them as expected when titlePosition is "after"', () => { - wrapper = render( - { - title: "Tab 1", - siblings: TEST_SIBLINGS, - titlePosition: "after", - }, - mount - ); - - expect( - wrapper.find(StyledTitleContent).props().children[0][1].props.children - ).toEqual("Tab 1"); - assertStyleMatch( - { - padding: "10px 16px", - }, - wrapper.find(StyledTitleContent) - ); - }); - - it('does not adjust padding when isTabSelected is true and position is "left"', () => { - wrapper = render( - { - title: "Tab 1", - siblings: TEST_SIBLINGS, - titlePosition: "before", - isTabSelected: true, - position: "left", - }, - mount - ); - - expect(wrapper.find(StyledTitleContent).props().hasSiblings).toEqual( - true - ); - expect( - wrapper.find(StyledTitleContent).props().children[0][0].props.children - ).toEqual("Tab 1"); - assertStyleMatch( - { padding: "10px 16px" }, - wrapper.find(StyledTitleContent) - ); - }); - - it('renders as expected when size is "large"', () => { - wrapper = render( - { - title: "Tab 1", - siblings: TEST_SIBLINGS, - titlePosition: "before", - size: "large", - }, - mount - ); - - expect( - wrapper.find(StyledTitleContent).props().children[0][0].props.children - ).toEqual("Tab 1"); - assertStyleMatch( - { - padding: "14px 24px", - }, - wrapper.find(StyledTitleContent) - ); - }); - - it('renders as expected when size is "large" and position is "left"', () => { - wrapper = render( - { - title: "Tab 1", - siblings: TEST_SIBLINGS, - titlePosition: "before", - size: "large", - position: "left", - }, - mount - ); - - expect( - wrapper.find(StyledTitleContent).props().children[0][0].props.children - ).toEqual("Tab 1"); - assertStyleMatch( - { - padding: "14px 24px", - }, - wrapper.find(StyledTitleContent) - ); - }); - }); - - describe("when tab has info", () => { - describe('when positioned "top"', () => { - it("applies proper styling", () => { - wrapper = render({ info: true }, mount); - assertStyleMatch( - { - borderBottomColor: "transparent", - outline: "1px solid", - outlineOffset: "-1px", - outlineColor: "var(--colorsSemanticInfo500)", - zIndex: "2", - }, - wrapper.find(StyledTitleContent) - ); - - assertStyleMatch( - { - borderBottomColor: "transparent", - outline: "1px solid", - outlineOffset: "-1px", - outlineColor: "var(--colorsSemanticInfo500)", - }, - wrapper.find(StyledTitleContent), - { modifier: ":hover" } - ); - }); - - it('applies proper styling when size is "large"', () => { - wrapper = render({ info: true, size: "large" }, mount); - assertStyleMatch( - { - borderBottomColor: "transparent", - outline: "1px solid", - outlineOffset: "-1px", - outlineColor: "var(--colorsSemanticInfo500)", - zIndex: "2", - }, - wrapper.find(StyledTitleContent) - ); - - assertStyleMatch( - { - borderBottomColor: "transparent", - outline: "1px solid", - outlineOffset: "-1px", - outlineColor: "var(--colorsSemanticInfo500)", - }, - wrapper.find(StyledTitleContent), - { modifier: ":hover" } - ); - }); - - it("renders the ValidationIcon", () => { - wrapper = render({ info: true, infoMessage: "foo" }, mount); - const icon = wrapper.find(ValidationIcon); - - expect(icon.exists()).toBeTruthy(); - expect(icon.props().info).toEqual("foo"); - - assertStyleMatch({ zIndex: "10" }, wrapper.find(StyledLayoutWrapper), { - modifier: `${StyledValidationIcon}`, - }); - - assertStyleMatch( - { - height: "16px", - top: "3px", - left: "-2px", - }, - wrapper.find(StyledLayoutWrapper), - { - modifier: `${StyledValidationIcon} ${StyledIcon}`, - } - ); - }); - }); - - describe('when positioned "left"', () => { - it('applies proper styling when position is "left"', () => { - wrapper = render({ position: "left", info: true }, mount); - assertStyleMatch( - { - borderRightColor: "transparent", - outline: "1px solid", - outlineOffset: "-1px", - outlineColor: "var(--colorsSemanticInfo500)", - paddingRight: "18px", - zIndex: "2", - }, - wrapper.find(StyledTitleContent) - ); - - assertStyleMatch( - { - borderRightColor: "transparent", - outline: "1px solid", - outlineOffset: "-1px", - outlineColor: "var(--colorsSemanticInfo500)", - paddingRight: "18px", - }, - wrapper.find(StyledTitleContent), - { modifier: ":hover" } - ); - }); - - it('applies proper styling when size is "large"', () => { - wrapper = render( - { position: "left", info: true, size: "large" }, - mount - ); - assertStyleMatch( - { - borderRightColor: "transparent", - outline: "1px solid", - outlineOffset: "-1px", - outlineColor: "var(--colorsSemanticInfo500)", - paddingRight: "26px", - zIndex: "2", - }, - wrapper.find(StyledTitleContent) - ); - - assertStyleMatch( - { - borderRightColor: "transparent", - outline: "1px solid", - outlineOffset: "-1px", - outlineColor: "var(--colorsSemanticInfo500)", - paddingRight: "26px", - }, - wrapper.find(StyledTitleContent), - { modifier: ":hover" } - ); - }); - }); - }); - - describe("when tab has warning", () => { - describe('when positioned "top"', () => { - it("applies proper styling", () => { - wrapper = render({ warning: true }, mount); - assertStyleMatch( - { - borderBottomColor: "transparent", - outline: "1px solid", - outlineOffset: "-1px", - outlineColor: "var(--colorsSemanticCaution500)", - zIndex: "2", - }, - wrapper.find(StyledTitleContent) - ); - - assertStyleMatch( - { - borderBottomColor: "transparent", - outline: "1px solid", - outlineOffset: "-1px", - outlineColor: "var(--colorsSemanticCaution500)", - }, - wrapper.find(StyledTitleContent), - { modifier: ":hover" } - ); - }); - - it('applies proper styling when size is "large"', () => { - wrapper = render({ warning: true, size: "large" }, mount); - assertStyleMatch( - { - borderBottomColor: "transparent", - outline: "1px solid", - outlineOffset: "-1px", - outlineColor: "var(--colorsSemanticCaution500)", - zIndex: "2", - }, - wrapper.find(StyledTitleContent) - ); - - assertStyleMatch( - { - borderBottomColor: "transparent", - outline: "1px solid", - outlineOffset: "-1px", - outlineColor: "var(--colorsSemanticCaution500)", - }, - wrapper.find(StyledTitleContent), - { modifier: ":hover" } - ); - }); - - it("renders the ValidationIcon", () => { - wrapper = render({ warning: true, warningMessage: "foo" }, mount); - const icon = wrapper.find(ValidationIcon); - - expect(icon.exists()).toBeTruthy(); - expect(icon.props().warning).toEqual("foo"); - - assertStyleMatch({ zIndex: "10" }, wrapper.find(StyledLayoutWrapper), { - modifier: `${StyledValidationIcon}`, - }); - - assertStyleMatch( - { - height: "16px", - top: "3px", - left: "-2px", - }, - wrapper.find(StyledLayoutWrapper), - { - modifier: `${StyledValidationIcon} ${StyledIcon}`, - } - ); - }); - }); - - describe('when positioned "left"', () => { - it('applies proper styling when position is "left"', () => { - wrapper = render({ position: "left", warning: true }, mount); - assertStyleMatch( - { - borderRightColor: "transparent", - outline: "1px solid", - outlineOffset: "-1px", - outlineColor: "var(--colorsSemanticCaution500)", - paddingRight: "18px", - zIndex: "2", - }, - wrapper.find(StyledTitleContent) - ); - - assertStyleMatch( - { - borderRightColor: "transparent", - outline: "1px solid", - outlineOffset: "-1px", - outlineColor: "var(--colorsSemanticCaution500)", - paddingRight: "18px", - }, - wrapper.find(StyledTitleContent), - { modifier: ":hover" } - ); - }); - - it('applies proper styling when size is "large"', () => { - wrapper = render( - { position: "left", warning: true, size: "large" }, - mount - ); - assertStyleMatch( - { - borderRightColor: "transparent", - outline: "1px solid", - outlineOffset: "-1px", - outlineColor: "var(--colorsSemanticCaution500)", - paddingRight: "26px", - zIndex: "2", - }, - wrapper.find(StyledTitleContent) - ); - - assertStyleMatch( - { - borderRightColor: "transparent", - outline: "1px solid", - outlineOffset: "-1px", - outlineColor: "var(--colorsSemanticCaution500)", - paddingRight: "26px", - }, - wrapper.find(StyledTitleContent), - { modifier: ":hover" } - ); - }); - }); - }); - - describe("when tab has error", () => { - describe('when positioned "top"', () => { - it("applies proper styling", () => { - wrapper = render({ error: true }, mount); - assertStyleMatch( - { - borderBottomColor: "transparent", - outline: "2px solid var(--colorsSemanticNegative500)", - outlineOffset: "-2px", - zIndex: "2", - }, - wrapper.find(StyledTitleContent) - ); - - assertStyleMatch( - { - borderBottomColor: "transparent", - outline: "2px solid var(--colorsSemanticNegative500)", - outlineOffset: "-2px", - }, - wrapper.find(StyledTitleContent), - { modifier: ":hover" } - ); - }); - - it('applies proper styling when size is "large"', () => { - wrapper = render({ error: true, size: "large" }, mount); - assertStyleMatch( - { - borderBottomColor: "transparent", - outline: "2px solid var(--colorsSemanticNegative500)", - outlineOffset: "-2px", - zIndex: "2", - }, - wrapper.find(StyledTitleContent) - ); - - assertStyleMatch( - { - borderBottomColor: "transparent", - outline: "2px solid var(--colorsSemanticNegative500)", - outlineOffset: "-2px", - }, - wrapper.find(StyledTitleContent), - { modifier: ":hover" } - ); - }); - - it("renders the ValidationIcon", () => { - wrapper = render({ error: true, errorMessage: "foo" }, mount); - const icon = wrapper.find(ValidationIcon); - expect(icon.exists()).toBeTruthy(); - expect(icon.props().error).toEqual("foo"); - - assertStyleMatch({ zIndex: "10" }, wrapper.find(StyledLayoutWrapper), { - modifier: `${StyledValidationIcon}`, - }); - - assertStyleMatch( - { - height: "16px", - top: "3px", - left: "-2px", - }, - wrapper.find(StyledLayoutWrapper), - { - modifier: `${StyledValidationIcon} ${StyledIcon}`, - } - ); - }); - }); - - describe('when positioned "left"', () => { - it("applies proper styling", () => { - wrapper = render({ position: "left", error: true }, mount); - assertStyleMatch( - { - borderRightColor: "transparent", - outline: "2px solid var(--colorsSemanticNegative500)", - outlineOffset: "-2px", - paddingRight: "18px", - zIndex: "2", - }, - wrapper.find(StyledTitleContent) - ); - - assertStyleMatch( - { - borderRightColor: "transparent", - outline: "2px solid var(--colorsSemanticNegative500)", - outlineOffset: "-2px", - paddingRight: "18px", - }, - wrapper.find(StyledTitleContent), - { modifier: ":hover" } - ); - }); - - it('applies proper styling when size is "large"', () => { - wrapper = render( - { position: "left", error: true, size: "large" }, - mount - ); - assertStyleMatch( - { - borderRightColor: "transparent", - outline: "2px solid var(--colorsSemanticNegative500)", - outlineOffset: "-2px", - paddingRight: "26px", - zIndex: "2", - }, - wrapper.find(StyledTitleContent) - ); - - assertStyleMatch( - { - borderRightColor: "transparent", - outline: "2px solid var(--colorsSemanticNegative500)", - outlineOffset: "-2px", - paddingRight: "26px", - }, - wrapper.find(StyledTitleContent), - { modifier: ":hover" } - ); - }); - }); - }); - - describe("noLeftBorder", () => { - it('sets border-left to "none"', () => { - wrapper = render( - { - theme: sageTheme, - size: "default", - borders: true, - noLeftBorder: true, - }, - mount - ); - - assertStyleMatch( - { borderLeft: "none" }, - wrapper.find(StyledTitleContent) - ); - }); - }); - - describe("noRightBorder", () => { - it('sets border-right to "none"', () => { - wrapper = render( - { - theme: sageTheme, - size: "default", - borders: true, - noRightBorder: true, - }, - mount - ); - - assertStyleMatch( - { borderRight: "none" }, - wrapper.find(StyledTitleContent) - ); - }); - }); - - describe("setting alternateStyling prop", () => { - describe("when TabTitle is selected, focused or hovered", () => { - it.each(["default", "large"] as const)( - "applies the correct background-color when size is %s", - (size) => { - wrapper = render({ alternateStyling: true, size }, mount); - assertStyleMatch( - { - backgroundColor: "var(--colorsActionMinor200)", - }, - wrapper.find(StyledTabTitleButton), - { modifier: ":focus" } - ); - - assertStyleMatch( - { - backgroundColor: "var(--colorsActionMinor250)", - }, - wrapper.find(StyledTabTitleButton), - { modifier: ":hover" } - ); - - assertStyleMatch( - { - backgroundColor: "var(--colorsActionMinor200)", - }, - render( - { alternateStyling: true, isTabSelected: true, size }, - mount - ).find(StyledTabTitleButton) - ); - } - ); - - it('overrides the border-right-color when position is "left"', () => { - wrapper = render({ alternateStyling: true, position: "left" }, mount); - - assertStyleMatch( - { - borderRightColor: "var(--colorsActionMinor100)", - }, - wrapper.find(StyledTabTitleButton), - { modifier: ":hover" } - ); - }); - - it("applies proper styling when borders prop is true", () => { - wrapper = render( - { - borders: true, - isTabSelected: true, - position: "left", - alternateStyling: true, - }, - mount - ); - - assertStyleMatch( - { - borderTop: "1px solid var(--colorsActionMinor100)", - borderLeft: "1px solid var(--colorsActionMinor100)", - borderBottom: "1px solid var(--colorsActionMinor100)", - }, - wrapper.find(StyledTitleContent) - ); - }); - - it('overrides the border-right-color when position is "left" and isTabSelected is true', () => { - wrapper = render( - { alternateStyling: true, position: "left", isTabSelected: true }, - mount - ); - - assertStyleMatch( - { - borderRightColor: "var(--colorsActionMinor100)", - }, - wrapper.find(StyledTabTitleButton) - ); - - assertStyleMatch( - { - borderRightColor: "var(--colorsActionMinor100)", - }, - wrapper.find(StyledTabTitleButton), - { modifier: ":hover" } - ); - }); - }); - }); - - describe("Click event on title content", () => { - it("calls the handler", () => { - const onClick = jest.fn(); - const stopPropagation = jest.fn(); - const preventDefault = jest.fn(); - const customEvent = { - preventDefault, - stopPropagation, - target: { dataset: { tabid: "uniqueid1" } }, - }; - wrapper = render( - { onClick, ref: { current: { focus: jest.fn() } } }, - mount - ); - - wrapper - .find(StyledTabTitleButton) - .props() - .onClick({ stopPropagation, target: {}, preventDefault }); - expect(onClick).toHaveBeenCalledWith(customEvent); - }); - }); - - describe("customLayout", () => { - describe('when position is "top"', () => { - it("renders the correct styles when size is default", () => { - wrapper = render({ customLayout:
foo
}, mount); - - expect( - wrapper.find(StyledTitleContent).props().hasCustomLayout - ).toEqual(true); - - assertStyleMatch( - { - display: "flex", - padding: "0px", - }, - wrapper.find(StyledTitleContent) - ); - }); - - it('renders the correct styles when size is "default" and isTabSelected is true', () => { - wrapper = render( - { customLayout:
foo
, isTabSelected: true }, - mount - ); - - expect( - wrapper.find(StyledTitleContent).props().hasCustomLayout - ).toEqual(true); - - assertStyleMatch( - { - paddingBottom: "0px", - }, - wrapper.find(StyledTitleContent) - ); - }); - - it('renders the correct styles when size is "default" and has error', () => { - wrapper = render({ customLayout:
foo
, error: true }, mount); - - expect( - wrapper.find(StyledTitleContent).props().hasCustomLayout - ).toEqual(true); - - assertStyleMatch( - { - paddingBottom: "2px", - paddingRight: "14px", - }, - wrapper.find(StyledTitleContent) - ); - - assertStyleMatch( - { - paddingBottom: "2px", - }, - wrapper.find(StyledTitleContent), - { modifier: ":hover" } - ); - }); - - it('renders the correct styles when size is "large"', () => { - wrapper = render( - { customLayout:
foo
, size: "large" }, - mount - ); - - expect( - wrapper.find(StyledTitleContent).props().hasCustomLayout - ).toEqual(true); - - assertStyleMatch( - { - display: "flex", - padding: "2px", - }, - wrapper.find(StyledTitleContent) - ); - }); - - it('renders the correct styles when size is "large" and has warning', () => { - wrapper = render( - { customLayout:
foo
, size: "large", warning: true }, - mount - ); - - expect( - wrapper.find(StyledTitleContent).props().hasCustomLayout - ).toEqual(true); - - assertStyleMatch( - { - paddingBottom: "4px", - paddingRight: "18px", - }, - wrapper.find(StyledTitleContent) - ); - - assertStyleMatch( - { - paddingBottom: "4px", - }, - wrapper.find(StyledTitleContent), - { modifier: ":hover" } - ); - }); - - describe('when position is "left"', () => { - it('renders the correct styles when size is "default"', () => { - wrapper = render( - { customLayout:
foo
, position: "left" }, - mount - ); - - expect( - wrapper.find(StyledTitleContent).props().hasCustomLayout - ).toEqual(true); - - assertStyleMatch( - { - display: "flex", - padding: "0px", - }, - wrapper.find(StyledTitleContent) - ); - }); - - it('renders the correct styles when size is "default" and isTabSelected is true', () => { - wrapper = render( - { - customLayout:
foo
, - position: "left", - isTabSelected: true, - }, - mount - ); - - expect( - wrapper.find(StyledTitleContent).props().hasCustomLayout - ).toEqual(true); - - assertStyleMatch( - { - paddingRight: "0px", - }, - wrapper.find(StyledTitleContent) - ); - }); - - it('renders the correct styles when size is "default" and has error', () => { - wrapper = render( - { customLayout:
foo
, error: true, position: "left" }, - mount - ); - - expect( - wrapper.find(StyledTitleContent).props().hasCustomLayout - ).toEqual(true); - - assertStyleMatch( - { - paddingRight: "18px", - }, - wrapper.find(StyledTitleContent) - ); - }); - - it('renders the correct styles when size is "large"', () => { - wrapper = render( - { customLayout:
foo
, position: "left", size: "large" }, - mount - ); - - expect( - wrapper.find(StyledTitleContent).props().hasCustomLayout - ).toEqual(true); - - assertStyleMatch( - { - display: "flex", - padding: "2px", - }, - wrapper.find(StyledTitleContent) - ); - }); - - it('renders the correct styles when size is "large" and has warning', () => { - wrapper = render( - { - customLayout:
foo
, - position: "left", - size: "large", - warning: true, - }, - mount - ); - - expect( - wrapper.find(StyledTitleContent).props().hasCustomLayout - ).toEqual(true); - - assertStyleMatch( - { - paddingRight: "26px", - }, - wrapper.find(StyledTitleContent) - ); - }); - }); - }); - }); - - describe("TabTitleContext", () => { - let container: HTMLDivElement | null; - - beforeEach(() => { - container = document.createElement("div"); - container.id = "enzymeContainer"; - document.body.appendChild(container); - - wrapper = render( - { - siblings: [], - ref: { current: null }, - }, - mount, - { - attachTo: document.getElementById("enzymeContainer"), - } - ); - }); - - afterEach(() => { - if (container && container.parentNode) { - container.parentNode.removeChild(container); - } - - container = null; - }); - - it("sets showTooltip to true when the TabTitle is focused", () => { - act(() => { - wrapper.find(StyledTabTitleButton).prop("onFocus")(); - }); - - expect(wrapper.update().find(Tooltip).prop("isVisible")).toBe(true); - }); - - it("sets showTooltip to true when the TabTitle is hovered", () => { - act(() => { - wrapper.find(StyledTabTitleButton).prop("onMouseOver")(); - }); - - expect(wrapper.update().find(Tooltip).prop("isVisible")).toBe(true); - }); - - it("sets showTooltip to false when the TabTitle is blurred", () => { - act(() => { - wrapper.find(StyledTabTitleButton).prop("onFocus")(); - wrapper.update().find(StyledTabTitleButton).prop("onBlur")(); - }); - - expect(wrapper.update().find(Tooltip).prop("isVisible")).toBe(false); - }); - - it("sets showTooltip to false when the TabTitle is not hovered", () => { - act(() => { - wrapper.find(StyledTabTitleButton).prop("onMouseOver")(); - wrapper.update().find(StyledTabTitleButton).prop("onMouseLeave")(); - }); - - expect(wrapper.update().find(Tooltip).prop("isVisible")).toBe(false); - }); - - it("does not set showTooltip to false when TabTitle is blurred but still hovered with mouse", () => { - act(() => { - wrapper.find(StyledTabTitleButton).prop("onMouseOver")(); - wrapper.update().find(StyledTabTitleButton).prop("onBlur")(); - }); - - expect(wrapper.update().find(Tooltip).prop("isVisible")).toBe(true); - }); - }); - - it.each(["top", "left"])( - "has the expected border radius styling when position is %s", - (position) => { - const radiusStyling = - position === "top" - ? { - borderTopLeftRadius: "var(--borderRadius100)", - borderTopRightRadius: "var(--borderRadius100)", - borderBottomRightRadius: "var(--borderRadius000)", - borderBottomLeftRadius: "var(--borderRadius000)", - } - : { - borderTopLeftRadius: "var(--borderRadius100)", - borderTopRightRadius: "var(--borderRadius000)", - borderBottomRightRadius: "var(--borderRadius000)", - borderBottomLeftRadius: "var(--borderRadius100)", - }; - assertStyleMatch( - radiusStyling, - render({ position }, mount).find(StyledTitleContent) - ); - } - ); -}); diff --git a/src/components/tabs/__internal__/tab-title/tab-title.test.tsx b/src/components/tabs/__internal__/tab-title/tab-title.test.tsx new file mode 100644 index 0000000000..ca61fb6135 --- /dev/null +++ b/src/components/tabs/__internal__/tab-title/tab-title.test.tsx @@ -0,0 +1,612 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import TabTitle from "./tab-title.component"; + +test("renders the component as a tab with text passed as `title` prop", () => { + render( + {}} onKeyDown={() => {}} /> + ); + + const tabTitle = screen.getByRole("tab"); + expect(tabTitle).toBeVisible(); + expect(tabTitle).toHaveTextContent("example title"); +}); + +test("accepts `className` as a prop", () => { + render( + {}} + onKeyDown={() => {}} + /> + ); + + const tabTitle = screen.getByRole("tab"); + expect(tabTitle).toHaveClass("class1", "class2"); +}); + +test("renders with the expected `data-element` attribute", () => { + render( {}} onKeyDown={() => {}} />); + + expect(screen.getByRole("tab")).toHaveAttribute("data-element", "select-tab"); +}); + +test("renders with `data-tabid` attribute set to `dataTabId` prop", () => { + render( + {}} + onKeyDown={() => {}} + /> + ); + + expect(screen.getByRole("tab")).toHaveAttribute( + "data-tabid", + "unique-tab-id" + ); +}); + +test("renders with `data-role` attribute set to the prop value", () => { + render( + {}} + onKeyDown={() => {}} + /> + ); + + expect(screen.getByRole("tab")).toHaveAttribute( + "data-role", + "custom-data-role" + ); +}); + +test.each([ + ["Enter", "{Enter}"], + ["Space", " "], +])( + "when the `href` prop is provided, the specified URL should open in a new tab if the %s key is pressed with the tab title focused", + async (_, key) => { + const globalOpenMock = jest + .spyOn(global, "open") + .mockImplementation(() => null); + const user = userEvent.setup(); + render( + {}} onKeyDown={() => {}} /> + ); + + screen.getByRole("tab").focus(); + await user.keyboard(key); + + expect(globalOpenMock).toHaveBeenCalledWith("randomUrl", "_blank"); + globalOpenMock.mockRestore(); + } +); + +test("when the `href` prop is provided, the specified URL should open in a new tab on click", async () => { + const globalOpenMock = jest + .spyOn(global, "open") + .mockImplementation(() => null); + const user = userEvent.setup(); + render( {}} onKeyDown={() => {}} />); + + await user.click(screen.getByRole("tab")); + + expect(globalOpenMock).toHaveBeenCalledWith("randomUrl", "_blank"); + globalOpenMock.mockRestore(); +}); + +test.each([ + ["Enter", "{Enter}"], + ["Space", " "], +])( + "when the `href` prop is not provided, no new tab should be opened when %s is pressed", + async (_, key) => { + const globalOpenMock = jest + .spyOn(global, "open") + .mockImplementation(() => null); + const user = userEvent.setup(); + render( {}} onKeyDown={() => {}} />); + + screen.getByRole("tab").focus(); + await user.keyboard(key); + + expect(globalOpenMock).not.toHaveBeenCalled(); + globalOpenMock.mockRestore(); + } +); + +test("when the `isTabSelected` prop is set, the `aria-selected` attribute is set to `true`", () => { + render( {}} onKeyDown={() => {}} />); + + expect(screen.getByRole("tab")).toHaveAttribute("aria-selected", "true"); +}); + +test("when the `isTabSelected` prop is not set, the `aria-selected` attribute is not set", () => { + render( {}} onKeyDown={() => {}} />); + + expect(screen.getByRole("tab")).not.toHaveAttribute("aria-selected"); +}); + +test("`siblings` are rendered after the title when `titlePosition` is `before`", () => { + render( + bar} + titlePosition="before" + onClick={() => {}} + onKeyDown={() => {}} + /> + ); + + expect(screen.getByTestId("sibling")).toBeVisible(); + expect(screen.getByTestId("sibling")).toHaveTextContent("bar"); + expect(screen.getByRole("tab")).toHaveTextContent("foobar"); +}); + +test("`siblings` are rendered before the title when `titlePosition` is `after`", () => { + render( + bar} + titlePosition="after" + onClick={() => {}} + onKeyDown={() => {}} + /> + ); + + expect(screen.getByTestId("sibling")).toBeVisible(); + expect(screen.getByTestId("sibling")).toHaveTextContent("bar"); + expect(screen.getByRole("tab")).toHaveTextContent("barfoo"); +}); + +test("`siblings` are rendered after the title when `titlePosition` is not passed", () => { + render( + bar} + onClick={() => {}} + onKeyDown={() => {}} + /> + ); + + expect(screen.getByTestId("sibling")).toBeVisible(); + expect(screen.getByTestId("sibling")).toHaveTextContent("bar"); + expect(screen.getByRole("tab")).toHaveTextContent("foobar"); +}); + +test("calls the `onClick` prop when clicked", async () => { + const user = userEvent.setup(); + const onClick = jest.fn(); + render( + {}} /> + ); + + await user.click(screen.getByRole("tab")); + + expect(onClick).toHaveBeenCalledTimes(1); + expect(onClick).toHaveBeenCalledWith( + expect.objectContaining({ + target: expect.objectContaining({ dataset: { tabid: "uniqueid1" } }), + }) + ); +}); + +test.each(["error", "warning", "info"])( + "displays a tooltip on focus when %s validation is failed", + (validationType) => { + render( + {}} + onKeyDown={() => {}} + {...{ + [`${validationType}Message`]: "validation message", + [validationType]: true, + }} + /> + ); + + screen.getByRole("tab").focus(); + + expect(screen.getByTestId("tooltip")).toBeVisible(); + expect(screen.getByTestId("tooltip")).toHaveTextContent( + "validation message" + ); + } +); + +test.each(["error", "warning", "info"])( + "hides the tooltip on blur when %s validation is failed", + async (validationType) => { + const user = userEvent.setup(); + render( + {}} + onKeyDown={() => {}} + {...{ + [`${validationType}Message`]: "validation message", + [validationType]: true, + }} + /> + ); + + screen.getByRole("tab").focus(); + await user.tab(); + + expect(screen.queryByTestId("tooltip")).not.toBeInTheDocument(); + expect(screen.queryByText("validation message")).not.toBeInTheDocument(); + } +); + +test.each(["error", "warning", "info"])( + "displays a tooltip on hover when %s validation is failed", + async (validationType) => { + const user = userEvent.setup(); + render( + {}} + onKeyDown={() => {}} + {...{ + [`${validationType}Message`]: "validation message", + [validationType]: true, + }} + /> + ); + + await user.hover(screen.getByRole("tab")); + + expect(screen.getByTestId("tooltip")).toBeVisible(); + expect(screen.getByTestId("tooltip")).toHaveTextContent( + "validation message" + ); + } +); + +test.each(["error", "warning", "info"])( + "hides the tooltip on mouse leave when %s validation is failed", + async (validationType) => { + const user = userEvent.setup(); + render( + {}} + onKeyDown={() => {}} + {...{ + [`${validationType}Message`]: "validation message", + [validationType]: true, + }} + /> + ); + + await user.hover(screen.getByRole("tab")); + await user.unhover(screen.getByRole("tab")); + + expect(screen.queryByTestId("tooltip")).not.toBeInTheDocument(); + expect(screen.queryByText("validation message")).not.toBeInTheDocument(); + } +); + +test.each(["error", "warning", "info"])( + "does not hide the tooltip on blur when already hovered if %s validation is failed", + async (validationType) => { + const user = userEvent.setup(); + render( + {}} + onKeyDown={() => {}} + {...{ + [`${validationType}Message`]: "validation message", + [validationType]: true, + }} + /> + ); + + await user.hover(screen.getByRole("tab")); + screen.getByRole("tab").focus(); + await user.tab(); + + expect(screen.getByTestId("tooltip")).toBeVisible(); + expect(screen.getByTestId("tooltip")).toHaveTextContent( + "validation message" + ); + } +); + +// coverage +test("renders the correct styles when a custom layout is used", () => { + render( + foo} + position="left" + info + isTabSelected + onClick={() => {}} + onKeyDown={() => {}} + /> + ); + + expect(screen.getByTestId("my-custom-layout")).toBeVisible(); + expect(screen.getByTestId("my-custom-layout")).toHaveTextContent("foo"); +}); + +// coverage +test("renders as expected when `position` prop is `left` and `align` prop is `right`", () => { + render( + {}} + onKeyDown={() => {}} + /> + ); + + expect(screen.getByTestId("tab-title-content")).toHaveStyle({ + "justify-content": "flex-end", + }); +}); + +// coverage +test("renders as expected when `size` prop is `large` and `position` prop is `top`", () => { + render( + {}} + onKeyDown={() => {}} + /> + ); + + expect(screen.getByTestId("tab-title-content")).toHaveStyle({ + padding: "14px 24px", + "font-size": "16px", + }); +}); + +// coverage +test("renders as expected when `size` prop is `large` and `position` prop is `left`", () => { + render( + {}} + onKeyDown={() => {}} + /> + ); + + expect(screen.getByTestId("tab-title-content")).toHaveStyle({ + padding: "14px 24px", + "font-size": "16px", + }); +}); + +// coverage +test("does not apply selected styling when it has error or warning", () => { + render( + {}} + onKeyDown={() => {}} + /> + ); + + expect( + screen.queryByTestId("tab-selected-indicator") + ).not.toBeInTheDocument(); +}); + +// coverage +test("applies proper styling when `position` prop is `left` and there is a warning message", () => { + render( + {}} onKeyDown={() => {}} /> + ); + + expect(screen.getByTestId("tab-title-content")).toHaveStyle({ + "border-right-color": "transparent", + outline: "1px solid", + "outline-offset": "-1px", + "padding-right": "18px", + zIndex: "2", + }); + expect(screen.getByTestId("tab-title-content")).toHaveStyleRule( + "outline-color", + "var(--colorsSemanticCaution500)" + ); + expect(screen.getByTestId("tab-title-content")).toHaveStyleRule( + "border-right-color", + "transparent", + { + modifier: ":hover", + } + ); + expect(screen.getByTestId("tab-title-content")).toHaveStyleRule( + "outline", + "1px solid", + { modifier: ":hover" } + ); + expect( + screen.getByTestId("tab-title-content") + ).toHaveStyleRule("outline-offset", "-1px", { modifier: ":hover" }); + expect(screen.getByTestId("tab-title-content")).toHaveStyleRule( + "outline-color", + "var(--colorsSemanticCaution500)", + { + modifier: ":hover", + } + ); + expect( + screen.getByTestId("tab-title-content") + ).toHaveStyleRule("padding-right", "18px", { modifier: ":hover" }); +}); + +// coverage +test("renders the correct styles when an error is present and a custom layout is used", () => { + render( + foo} + error + onClick={() => {}} + onKeyDown={() => {}} + /> + ); + + expect(screen.getByTestId("tab-title-content")).toHaveStyle({ + "padding-bottom": "2px", + "padding-right": "14px", + }); + expect( + screen.getByTestId("tab-title-content") + ).toHaveStyleRule("padding-bottom", "2px", { modifier: ":hover" }); +}); + +// coverage +test("renders the correct styles when size is `large`, a warning is present and a custom layout is used", () => { + render( + foo} + size="large" + warning + onClick={() => {}} + onKeyDown={() => {}} + /> + ); + + expect(screen.getByTestId("tab-title-content")).toHaveStyle({ + "padding-bottom": "4px", + "padding-right": "18px", + }); + + expect( + screen.getByTestId("tab-title-content") + ).toHaveStyleRule("padding-bottom", "4px", { modifier: ":hover" }); +}); + +// coverage +test("renders the correct styles when size is `large`, position is `left`, a warning is present and a custom layout is used", () => { + render( + foo} + size="large" + position="left" + warning + isTabSelected + onClick={() => {}} + onKeyDown={() => {}} + /> + ); + + expect(screen.getByTestId("tab-title-content")).toHaveStyle({ + "padding-right": "26px", + }); +}); + +// coverage +test("the `noLeftBorder` prop removes the left border", () => { + render( {}} onKeyDown={() => {}} />); + + expect(screen.getByTestId("tab-title-content")).toHaveStyle({ + "border-left": "none", + }); +}); + +// coverage +test("applies proper styling when `borders` prop is true and `position` is `left`", () => { + render( + {}} onKeyDown={() => {}} /> + ); + + expect(screen.getByTestId("tab-title-content")).toHaveStyleRule( + "border-top", + "1px solid var(--colorsActionMinor100)" + ); + expect(screen.getByTestId("tab-title-content")).toHaveStyleRule( + "border-left", + "1px solid var(--colorsActionMinor100)" + ); + expect(screen.getByTestId("tab-title-content")).toHaveStyleRule( + "border-bottom", + "1px solid var(--colorsActionMinor100)" + ); +}); + +// coverage +test("applies proper styling when `borders` prop is true and `size` is `large`", () => { + render( + {}} onKeyDown={() => {}} /> + ); + + expect(screen.getByTestId("tab-title-content")).toHaveStyleRule( + "border-top", + "1px solid var(--colorsActionMinor100)" + ); + expect(screen.getByTestId("tab-title-content")).toHaveStyleRule( + "border-left", + "1px solid var(--colorsActionMinor100)" + ); + expect(screen.getByTestId("tab-title-content")).toHaveStyleRule( + "border-bottom", + "1px solid var(--colorsActionMinor100)" + ); +}); + +// coverage +test("overrides the border-right-color when `alternateStyling` prop is `true` and `position` prop is `left`", () => { + render( + {}} + onKeyDown={() => {}} + /> + ); + + expect(screen.getByRole("tab")).toHaveStyleRule( + "border-right-color", + "var(--colorsActionMinor100)", + { modifier: ":hover" } + ); +}); + +// coverage +test("sets border-left to `none` when the `noLeftBorder` prop is set", () => { + render( + {}} onKeyDown={() => {}} /> + ); + + expect(screen.getByTestId("tab-title-content")).toHaveStyle({ + "border-left": "none", + }); +}); + +// coverage +test("sets border-right to `none` when the `noRightBorder` prop is set", () => { + render( + {}} onKeyDown={() => {}} /> + ); + + expect(screen.getByTestId("tab-title-content")).toHaveStyle({ + "border-right": "none", + }); +}); + +// coverage +test("renders the correct styles when a custom layout is used, `size` is `default` `isTabSelected` is `true`", () => { + render( + foo} + isTabSelected + onClick={() => {}} + onKeyDown={() => {}} + /> + ); + + expect(screen.getByTestId("tab-title-content")).toHaveStyleRule( + "padding-bottom", + "0px" + ); +}); diff --git a/src/components/tabs/__internal__/tabs-header/tabs-header.spec.tsx b/src/components/tabs/__internal__/tabs-header/tabs-header.spec.tsx deleted file mode 100644 index 17d41c04e1..0000000000 --- a/src/components/tabs/__internal__/tabs-header/tabs-header.spec.tsx +++ /dev/null @@ -1,298 +0,0 @@ -import React from "react"; -import { mount, shallow } from "enzyme"; - -import TabsHeader, { TabHeaderProps } from "./tabs-header.component"; -import { - StyledTabsHeaderWrapper, - StyledTabsHeaderList, - StyledTabsHeaderListProps, - StyledTabsBottomBorder, -} from "./tabs-header.style"; -import TabTitle from "../tab-title/tab-title.component"; -import { assertStyleMatch } from "../../../../__spec_helper__/__internal__/test-utils"; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function render(props: Partial = {}, renderer: any = shallow) { - return renderer( - - {}} onKeyDown={() => {}} /> - {}} onKeyDown={() => {}} /> - - ); -} - -function renderStyles(props: Partial = {}) { - return mount(); -} - -describe("TabsHeader", () => { - it("renders as expected", () => { - assertStyleMatch( - { - display: "flex", - cursor: "default", - listStyle: "none", - padding: "3px", - }, - renderStyles() - ); - }); - - it("renders children correctly", () => { - expect(render().find(TabTitle)).toHaveLength(2); - }); - - it("has the role of a role prop value", () => { - expect( - render({ role: "tablist" }).find(StyledTabsHeaderList).props().role - ).toEqual("tablist"); - }); - - describe("when position prop is set to left", () => { - it("applies proper styles", () => { - const wrapper = render({ position: "left" }, mount); - - assertStyleMatch( - { - flexDirection: "column", - boxShadow: `none`, - }, - wrapper.find(StyledTabsHeaderList) - ); - - assertStyleMatch( - { - minWidth: "20%", - overflowY: "auto", - padding: "3px", - }, - wrapper.find(StyledTabsHeaderWrapper) - ); - }); - - it("applies proper styles when noRightBorder true", () => { - assertStyleMatch( - { - boxShadow: "none", - }, - renderStyles({ position: "left", noRightBorder: true }) - ); - }); - - it("does not render the bottom border element", () => { - const wrapper = render({ position: "left" }, mount); - expect(wrapper.find(StyledTabsBottomBorder)).toHaveLength(0); - }); - }); - - describe("when position prop is set to top", () => { - it("renders the bottom border element", () => { - const wrapper = render({ position: "top" }, mount); - expect(wrapper.find(StyledTabsBottomBorder)).toHaveLength(1); - }); - }); - - describe("when align prop is set to right", () => { - it("applies proper styles", () => { - assertStyleMatch( - { - justifyContent: "flex-end", - textAlign: "right", - }, - renderStyles({ align: "right" }) - ); - }); - - it("applies proper styles when positioned left", () => { - assertStyleMatch( - { - justifyContent: "flex-start", - }, - renderStyles({ position: "left", align: "right" }) - ); - }); - }); - - describe("when extendedLine is false", () => { - it("applies proper styles", () => { - assertStyleMatch( - { - width: "fit-content", - }, - renderStyles({ extendedLine: false }) - ); - }); - }); - - describe("custom target styling", () => { - const wrapper = render({ isInSidebar: true, position: "left" }, mount); - assertStyleMatch( - { - minWidth: "100%", - }, - wrapper.find(StyledTabsHeaderWrapper) - ); - }); - - describe("horizontal scroll", () => { - const mockScroll = ({ - element, - ...values - }: { - element: HTMLElement; - scrollWidth?: number; - clientWidth?: number; - scrollLeft?: number; - }) => { - Object.entries(values).forEach(([key, value]) => { - Object.defineProperty(element, key, { - configurable: true, - value, - }); - }); - }; - - describe("when is scrollable", () => { - it("renders before and after pseudoelements", () => { - const wrapper = render({}, mount); - - const list = wrapper.find(StyledTabsHeaderList).getDOMNode(); - mockScroll({ - element: list, - scrollWidth: 512, - clientWidth: 256, - }); - - wrapper.setProps({}); - - assertStyleMatch( - { - pointerEvents: "none", - content: '""', - backgroundRepeat: "no-repeat", - backgroundSize: "16px 48px", - backgroundAttachment: "scroll", - zIndex: "1000", - position: "sticky", - minWidth: "16px", - transition: "opacity 0.1s ease-in-out", - background: - "radial-gradient( farthest-side at 0 50%, rgba(0,0,0,0.2), rgba(0,0,0,0) )", - backgroundPosition: "left calc(50% - 4px)", - left: "-3px", - marginRight: "-16px", - }, - list, - { modifier: ":before" } - ); - - assertStyleMatch( - { - pointerEvents: "none", - content: '""', - backgroundRepeat: "no-repeat", - backgroundSize: "16px 48px", - backgroundAttachment: "scroll", - zIndex: "1000", - position: "sticky", - minWidth: "16px", - transition: "opacity 0.1s ease-in-out", - background: - "radial-gradient( farthest-side at 100% 50%, rgba(0,0,0,0.2), rgba(0,0,0,0) )", - backgroundPosition: "right calc(50% - 4px)", - right: "-3px", - marginLeft: "-16px", - }, - list, - { modifier: ":after" } - ); - }); - - it.each([ - [0, "0"], - [64, "0.5"], - [128, "1"], - [256, "1"], - ])( - "renders before element with correct opacity", - (scrollLeft, opacity) => { - const wrapper = render({}, mount); - const list = wrapper.find(StyledTabsHeaderList).getDOMNode(); - - mockScroll({ - element: list, - scrollWidth: 768, - clientWidth: 256, - scrollLeft, - }); - - wrapper.setProps({}); - - wrapper.find(StyledTabsHeaderList).simulate("scroll"); - - assertStyleMatch( - { - opacity, - }, - list, - { modifier: ":before" } - ); - } - ); - - it.each([ - [256, "1"], - [384, "1"], - [448, "0.5"], - [512, "0"], - ])( - "renders after element with correct opacity", - (scrollLeft, opacity) => { - const wrapper = render({}, mount); - const list = wrapper.find(StyledTabsHeaderList).getDOMNode(); - - mockScroll({ - element: list, - scrollWidth: 768, - clientWidth: 256, - scrollLeft, - }); - - wrapper.setProps({}); - - wrapper.find(StyledTabsHeaderList).simulate("scroll"); - - assertStyleMatch( - { - opacity, - }, - list, - { modifier: ":after" } - ); - } - ); - }); - - describe("when is not scrollable", () => { - it("does not render before and after pseudoelements", () => { - const wrapper = render({}, mount); - const list = wrapper.find(StyledTabsHeaderList).getDOMNode(); - - mockScroll({ - element: list, - scrollWidth: 256, - clientWidth: 256, - }); - - wrapper.setProps({}); - - expect(list).not.toHaveStyleRule("min-width", "16px", { - modifier: ":after", - }); - expect(list).not.toHaveStyleRule("min-width", "16px", { - modifier: ":before", - }); - }); - }); - }); -}); diff --git a/src/components/tabs/__internal__/tabs-header/tabs-header.test.tsx b/src/components/tabs/__internal__/tabs-header/tabs-header.test.tsx new file mode 100644 index 0000000000..a4829bdef7 --- /dev/null +++ b/src/components/tabs/__internal__/tabs-header/tabs-header.test.tsx @@ -0,0 +1,127 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; + +import TabsHeader from "./tabs-header.component"; +import { StyledTabsHeaderList } from "./tabs-header.style"; +import TabTitle from "../tab-title/tab-title.component"; + +test("renders children correctly", () => { + render( + + {}} onKeyDown={() => {}} /> + {}} onKeyDown={() => {}} /> + + ); + + expect(screen.getAllByRole("tab")).toHaveLength(2); + expect(screen.getAllByRole("tab")[0]).toHaveTextContent("title-1"); + expect(screen.getAllByRole("tab")[1]).toHaveTextContent("title-2"); +}); + +test("accepts a `role` prop", () => { + render( + + {}} onKeyDown={() => {}} /> + {}} onKeyDown={() => {}} /> + + ); + + expect(screen.getByRole("tablist")).toBeVisible(); + expect(screen.getByRole("tablist")).toHaveTextContent("title-1title-2"); +}); + +// coverage +test("renders before element with correct opacity when scroll position is not at the left", () => { + render( + + {}} onKeyDown={() => {}} /> + {}} onKeyDown={() => {}} /> + + ); + + const mockScrollValues = { + scrollWidth: 768, + clientWidth: 256, + scrollLeft: 64, + }; + + const tabList = screen.getByRole("tablist"); + + Object.entries(mockScrollValues).forEach(([key, value]) => { + Object.defineProperty(tabList, key, { + configurable: true, + value, + }); + }); + + fireEvent.scroll(tabList); + + expect(tabList).toHaveStyleRule("opacity", "0.5", { modifier: ":before" }); +}); + +// coverage +test("applies proper styles when the `extendedLine` prop is `false`", () => { + render( + + {}} onKeyDown={() => {}} /> + {}} onKeyDown={() => {}} /> + + ); + + expect(screen.getByRole("tablist")).toHaveStyle({ width: "fit-content" }); +}); + +// coverage +test("applies proper styles when the `align` prop is `right`", () => { + render( + + {}} onKeyDown={() => {}} /> + {}} onKeyDown={() => {}} /> + + ); + + expect(screen.getByRole("tablist")).toHaveStyle({ + "justify-content": "flex-end", + "text-align": "right", + }); +}); + +// coverage +test("applies proper styles when the `noRightBorder` prop is `true` and `position` is `left`", () => { + render( + + {}} onKeyDown={() => {}} /> + {}} onKeyDown={() => {}} /> + + ); + + expect(screen.getByRole("tablist")).toHaveStyle({ + "box-shadow": "none", + }); +}); + +// coverage +test("applies proper styles when the `align` prop is `right` and `position` is `left`", () => { + render( + + {}} onKeyDown={() => {}} /> + {}} onKeyDown={() => {}} /> + + ); + + expect(screen.getByRole("tablist")).toHaveStyle({ + "justify-content": "flex-start", + }); +}); + +// coverage - need to render the styled component directly to cover the default prop assignments +test("renders with correct styles", () => { + render(); + + expect(screen.getByRole("tablist")).toHaveStyle({ + display: "flex", + cursor: "default", + "list-style": "none", + padding: "3px", + }); +}); diff --git a/src/components/tabs/components.test-pw.tsx b/src/components/tabs/components.test-pw.tsx index 7f3e1f1eae..4571836824 100644 --- a/src/components/tabs/components.test-pw.tsx +++ b/src/components/tabs/components.test-pw.tsx @@ -5,6 +5,7 @@ import { Checkbox } from "../checkbox"; import Box from "../box"; import Icon from "../icon"; import Pill from "../pill"; +import Button from "../button"; import DrawerSidebarContext from "../drawer/__internal__/drawer-sidebar.context"; @@ -653,3 +654,22 @@ export const Responsive = () => { ); }; + +export const WithUpdatingChild = () => { + const [updated, setUpdated] = useState(false); + const onButtonClick = () => setUpdated((prev) => !prev); + + return ( + + + + Content for tab 1 + + + Content for tab 2{updated ? "Foo" : "Bar"} + + + + + ); +}; diff --git a/src/components/tabs/tab/tab.spec.tsx b/src/components/tabs/tab/tab.spec.tsx deleted file mode 100644 index af5bddab3d..0000000000 --- a/src/components/tabs/tab/tab.spec.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import React from "react"; -import TestRenderer from "react-test-renderer"; -import { mount } from "enzyme"; -import Tab, { TabProps } from "."; -import Textbox from "../../textbox"; -import StyledTab, { StyledTabProps } from "./tab.style"; -import { - assertStyleMatch, - testStyledSystemPadding, -} from "../../../__spec_helper__/__internal__/test-utils"; - -const updateErrors = jest.fn(); -const updateWarnings = jest.fn(); -const tabId = "uniqueid1"; -function render(props: Partial = {}) { - return mount( - -

TabContent 1

-

TabContent 2

-
- ); -} - -function renderWithValidation( - props: Partial & { - id?: string; - validations: Record; - } -) { - return mount( - - - - ); -} - -function renderStyles(props: StyledTabProps = {}) { - return TestRenderer.create(); -} - -describe("Tab", () => { - let wrapper; - - testStyledSystemPadding((props) => ( - - TabContent - - )); - - testStyledSystemPadding((props) => ( - - TabContent - - )); - - it("has display property equals to none", () => { - wrapper = renderStyles(); - assertStyleMatch( - { - display: "none", - }, - wrapper.toJSON() - ); - }); - - it("renders its children correctly", () => { - expect(render().find(StyledTab).find("div").children()).toHaveLength(2); - }); - - it("contains custom className if passed as a prop", () => { - wrapper = render({ className: "class" }); - expect(wrapper.find(".class").exists()).toEqual(true); - }); - - it("has a default role if not set", () => { - wrapper = render(); - expect(wrapper.find("[role='tabpanel']").exists()).toEqual(true); - }); - - it("has a custom role if provided", () => { - wrapper = render({ role: "anotherRole" }); - expect(wrapper.find("[role='anotherRole']").exists()).toEqual(true); - }); - - it("sets the aria-labelledby based on ariaLabelledBy prop", () => { - wrapper = render({ ariaLabelledby: "ariaLabelledby" }); - expect(wrapper.find("[aria-labelledby='ariaLabelledby']").exists()).toEqual( - true - ); - }); - - it("does not pass title prop down to DOM element", () => { - expect(render({}).getDOMNode().getAttribute("title")).toEqual(null); - }); - - describe("if `href` prop provided", () => { - it("should not render the content", () => { - wrapper = render({ href: "#" }); - - expect(wrapper.find("p").exists()).toBe(false); - }); - }); - - describe("when a tab is selected", () => { - it("applies display block property", () => { - wrapper = renderStyles({ - isTabSelected: true, - }); - - assertStyleMatch( - { - display: "block", - }, - wrapper.toJSON() - ); - }); - - describe("when position prop is set to left and the tab is selected", () => { - it("applies width of 80%", () => { - wrapper = renderStyles({ - isTabSelected: true, - position: "left", - }); - assertStyleMatch( - { - width: "calc(80% + var(--spacing075))", - }, - wrapper.toJSON() - ); - }); - }); - }); - - describe("Tab validation", () => { - describe("updateErrors", () => { - it("calls the parent tab context with the new state", () => { - wrapper = renderWithValidation({ - updateErrors, - updateWarnings, - id: "foo", - validations: { error: true }, - }); - expect(updateErrors).toHaveBeenCalledWith(tabId, { foo: true }); - }); - }); - - describe("updateWarnings", () => { - it("calls the parent tab context with the new state", () => { - wrapper = renderWithValidation({ - updateErrors, - updateWarnings, - id: "foo", - validations: { warning: true }, - }); - expect(updateWarnings).toHaveBeenCalledWith(tabId, { foo: true }); - }); - }); - }); -}); diff --git a/src/components/tabs/tab/tab.test.tsx b/src/components/tabs/tab/tab.test.tsx new file mode 100644 index 0000000000..f7cb249d83 --- /dev/null +++ b/src/components/tabs/tab/tab.test.tsx @@ -0,0 +1,135 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import Tab from "."; +import Textbox from "../../textbox"; +import StyledTab from "./tab.style"; +import { testStyledSystemPadding } from "../../../__spec_helper__/__internal__/test-utils"; + +testStyledSystemPadding((props) => ( + + TabContent + +)); + +testStyledSystemPadding((props) => ( + + TabContent + +)); + +test("has a role of `tabpanel` if none is specified", () => { + render(tab content); + + expect(screen.getByText("tab content")).toHaveAttribute("role", "tabpanel"); +}); + +test("is not visible by default", () => { + render(tab content); + + expect(screen.getByRole("tabpanel", { hidden: true })).not.toBeVisible(); +}); + +test("is visible when selected", () => { + render( + + tab content + + ); + + expect(screen.getByRole("tabpanel")).toBeVisible(); +}); + +test("renders its children correctly", () => { + render(tab content); + + expect(screen.getByText("tab content")).toBeInTheDocument(); +}); + +test("passes the `className` prop to the element", () => { + render( + + tab content + + ); + + expect(screen.getByRole("tabpanel", { hidden: true })).toHaveClass( + "foo-class", + "bar-class" + ); +}); + +test("can be given a custom role via the `role` prop", () => { + render( + + tab content + + ); + + expect(screen.getByText("tab content")).toHaveAttribute("role", "article"); +}); + +test("accepts an `ariaLabelledBy` prop to set the accessible name", () => { + render( + + tab content +

an accessible name

+
+ ); + + expect(screen.getByRole("tabpanel")).toHaveAccessibleName( + "an accessible name" + ); +}); + +test("the `title` prop is not passed to the DOM element", () => { + render( + + tab content + + ); + + expect(screen.getByRole("tabpanel", { hidden: true })).not.toHaveAttribute( + "title" + ); +}); + +test("does not render the children if the `href` prop is passed", () => { + render( + + tab content + + ); + + expect(screen.queryByText("tab content")).not.toBeInTheDocument(); +}); + +test("calls the `updateErrors` function prop when an error is present in a child component", () => { + const updateErrors = jest.fn(); + render( + + {}} id="bar" error /> + + ); + + expect(updateErrors).toHaveBeenCalledWith("foo", { bar: true }); +}); + +test("calls the `updateWarnings` function prop when a warning is present in a child component", () => { + const updateWarnings = jest.fn(); + render( + + {}} id="bar" warning /> + + ); + + expect(updateWarnings).toHaveBeenCalledWith("foo", { bar: true }); +}); + +// coverage - need to render the styled component directly to cover the default prop assignments +test("renders with correct styles", () => { + render(); + + expect(screen.getByTestId("styled-component")).toHaveStyle({ + display: "none", + }); +}); diff --git a/src/components/tabs/tabs-test.stories.tsx b/src/components/tabs/tabs-test.stories.tsx index 2124719e92..c0f0ca4c76 100644 --- a/src/components/tabs/tabs-test.stories.tsx +++ b/src/components/tabs/tabs-test.stories.tsx @@ -1,9 +1,17 @@ import React from "react"; -import { Tabs, Tab, TabsProps } from "."; +import { Tabs, Tab, TabsProps, TabProps } from "."; +import DrawerSidebarContext from "../drawer/__internal__/drawer-sidebar.context"; +import Box from "../box"; +import { allModes } from "../../../.storybook/modes"; export default { title: "Tabs/Test", - includeStories: ["Default"], + includeStories: [ + "Default", + "TabsInSidebar", + "TabsInSidebarPositionedLeft", + "WithHorizontalScrollbar", + ], parameters: { info: { disable: true }, chromatic: { @@ -111,3 +119,173 @@ export const Default = (args: TabsProps) => { }; Default.storyName = "default"; + +export const TabsInSidebar = ( + props: Partial & Partial +) => { + return ( + + + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + Content for tab 4 + + + Content for tab 5 + + + + + ); +}; + +TabsInSidebar.storyName = "Tabs in sidebar"; +TabsInSidebar.parameters = { + info: { disable: true }, + chromatic: { + disableSnapshot: false, + }, +}; + +export const TabsInSidebarPositionedLeft = ( + props: Partial & Partial +) => { + return ( + + + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + Content for tab 4 + + + Content for tab 5 + + + + + ); +}; + +TabsInSidebarPositionedLeft.storyName = "Tabs in sidebar positioned left"; +TabsInSidebarPositionedLeft.parameters = { + info: { disable: true }, + chromatic: { + disableSnapshot: false, + }, +}; + +export const WithHorizontalScrollbar = () => { + return ( + + {Array.from({ length: 30 }).map((_, index) => ( + + Content for tab {index + 1} + + ))} + + ); +}; + +WithHorizontalScrollbar.storyName = "With horizontal scrollbar"; +WithHorizontalScrollbar.parameters = { + chromatic: { + disableSnapshot: false, + modes: { + desktop: allModes.chromatic, + }, + }, +}; diff --git a/src/components/tabs/tabs.component.tsx b/src/components/tabs/tabs.component.tsx index d88d4b73a6..9b2a1a4619 100644 --- a/src/components/tabs/tabs.component.tsx +++ b/src/components/tabs/tabs.component.tsx @@ -182,6 +182,8 @@ const Tabs = ({ /** Handles the changing of tabs with the mouse */ const handleTabClick = (ev: React.MouseEvent) => { + // istanbul ignore if + // (code doesn't seem to be ever reached - FE-6835 raised to investigate and hopefully remove this) if (Event.isEventType(ev, "keydown")) { return; } @@ -381,6 +383,7 @@ const Tabs = ({ { }); }); }); + + test("when the children of a Tab update, the selected tab does not change", async ({ + mount, + page, + }) => { + await mount(); + + await page.getByRole("button").click(); + + await expect(page.getByText("Content for tab 2")).toBeVisible(); + await expect(page.getByText("Content for tab 1")).not.toBeVisible(); + }); }); diff --git a/src/components/tabs/tabs.spec.tsx b/src/components/tabs/tabs.spec.tsx deleted file mode 100644 index efc031e923..0000000000 --- a/src/components/tabs/tabs.spec.tsx +++ /dev/null @@ -1,1269 +0,0 @@ -/* eslint-disable react/prop-types */ -import React, { useEffect, useContext } from "react"; -import { mount, shallow, MountRendererProps, ReactWrapper } from "enzyme"; -import { act } from "react-dom/test-utils"; -import TabTitle from "./__internal__/tab-title/tab-title.component"; -import { Tabs, Tab, TabsProps } from "."; -import TabContext from "./tab/__internal__/tab.context"; -import rootTagTest from "../../__internal__/utils/helpers/tags/tags-specs"; -import StyledTabs, { StyledTabsProps } from "./tabs.style"; -import StyledTab from "./tab/tab.style"; -import { StyledTabTitleButton } from "./__internal__/tab-title/tab-title.style"; -import { - assertStyleMatch, - simulate, - testStyledSystemMargin, -} from "../../__spec_helper__/__internal__/test-utils"; -import { StyledTabsHeaderWrapper } from "./__internal__/tabs-header/tabs-header.style"; -import DrawerSidebarContext from "../drawer/__internal__/drawer-sidebar.context"; -import Textbox from "../textbox"; -import NumeralDate from "../numeral-date"; -import ValidationIcon, { - ValidationProps, -} from "../../__internal__/validations"; - -const mockTarget = (tabid: string) => - (({ dataset: { tabid } } as unknown) as HTMLElement); -const mockEvent = (type: string, tabId: string) => - (({ type, target: mockTarget(tabId) } as unknown) as React.MouseEvent< - HTMLButtonElement | HTMLAnchorElement - >); - -function render( - props: Partial = {}, - mountOptions: MountRendererProps = {} -) { - return mount( - - - TabContent - - - TabContent - - - TabContent - - , - mountOptions - ); -} - -function renderStyles(props: StyledTabsProps) { - return mount(); -} - -interface MockValidationProps { - one?: string | boolean; - two?: string | boolean; - three?: string | boolean; - four?: string | boolean; -} - -interface TabChildrenProps extends ValidationProps { - id: string; - text?: string | boolean; -} - -const TabChildren = ({ id, error, warning, info, text }: TabChildrenProps) => { - const context = useContext(TabContext); - - useEffect(() => { - context.setError?.(id, !!error); - context.setWarning?.(id, !!warning); - context.setInfo?.(id, !!info); - }, [id, context, error, warning, info]); - - return ( -
- {text} -
- ); -}; - -type MockWrapperProps = Partial & { - errors?: MockValidationProps; - warnings?: MockValidationProps; - infos?: MockValidationProps; -}; - -const MockWrapper = ({ - errors = {}, - warnings = {}, - infos = {}, - validationStatusOverride = undefined, - renderHiddenTabs = true, -}: MockWrapperProps) => { - return ( - - - - - - - - - - - ); -}; - -describe("Tabs", () => { - testStyledSystemMargin((props) => ( - - - TabContent - - - )); - - testStyledSystemMargin((props) => ( - - - TabContent - - - )); - - // TODO: move this test into playwright FE-6425 - describe("when children of a Tab update", () => { - it("does not update the selected tab", () => { - const MockComponent = ({ - selectedTabId, - updateChild, - }: { - selectedTabId?: string; - updateChild: boolean; - }) => { - return ( - - - TabContent - - - {updateChild ? "Foo" : "Bar"} - - - ); - }; - const wrapper = mount( - - ); - - expect(wrapper.find(StyledTab).last().prop("isTabSelected")).toBe(true); - - wrapper.setProps({ updateChild: true }); - - expect( - wrapper.update().find(StyledTab).last().prop("isTabSelected") - ).toBe(true); - }); - }); - - describe("when used with NumeralDate as a child", () => { - it("should not throw", () => { - expect(() => { - mount( - - - - {}} /> - - - ); - }).not.toThrow(); - }); - }); - - describe("when `headerWidth` is provided", () => { - describe.each(["35%", "100px", "5em"])( - "and value of %s is provided", - (headerWidth) => { - it("should render correct `width` in `TabsHeader` component, and `Tab` `width` should be computed based on the headerWidth", () => { - const wrapper = mount( - - - TabContent - - - ); - - assertStyleMatch( - { - width: headerWidth, - }, - wrapper, - { - modifier: `${StyledTabsHeaderWrapper}`, - } - ); - - assertStyleMatch( - { - width: `calc(100% - ${headerWidth})`, - }, - wrapper, - { - modifier: `${StyledTab}`, - } - ); - }); - } - ); - }); - - describe('when `headerWidth` is provided, and `position="top"`', () => { - it("should render console error", () => { - const consoleSpy = jest - .spyOn(global.console, "error") - .mockImplementation(() => {}); - - mount( - - - TabContent - - - ); - - expect(consoleSpy).toHaveBeenCalledWith( - "Invalid usage of prop headerWidth in Tabs. The headerWidth can be used only if position is set to left" - ); - - consoleSpy.mockRestore(); - }); - }); - - describe("when passing custom className as a prop", () => { - it("adds it to the classList", () => { - const wrapper = render({ className: "class" }); - expect(wrapper.exists(".class")).toEqual(true); - }); - }); - - describe('When position is "left"', () => { - it("applies proper styling", () => { - const wrapper = renderStyles({ position: "left" }); - assertStyleMatch( - { - display: "flex", - width: "100%", - }, - wrapper - ); - }); - }); - - describe('When "selectedTabId" is passed a valid "tabId"', () => { - it("displays the specified Tab", () => { - const wrapper = render({ selectedTabId: "uniqueid2" }); - - expect(wrapper.find(Tab).at(1).props().isTabSelected).toEqual(true); - }); - }); - - describe("When renderHiddenTabs", () => { - describe("is false", () => { - it("renders only the currently visible tab", () => { - const tab = mount( - - -
-
- - -
-
- - -
-
- - - ).find(Tab); - - expect(tab).toHaveLength(1); - expect(tab.props().isTabSelected).toEqual(true); - expect(tab.props().title).toEqual("Tab Title 1"); - expect(tab.props().tabId).toEqual("uniqueid1"); - }); - - it.each(["error", "warning", "info"] as const)( - "adds the correct %s state to the tab header", - (validation) => { - const validationProp = { - [`${validation}s`]: { one: true }, - }; - const tabTitle = mount( - - ).find(TabTitle); - - expect(tabTitle.at(0).props()[validation]).toEqual(true); - expect(tabTitle.at(1).props()[validation]).toEqual(false); - } - ); - }); - - describe("is true", () => { - it("returns an array of all Tab components with the first selected", () => { - const tabs = mount( - - -
-
- - -
-
- - -
-
- - - ).find(Tab); - - expect(tabs).toHaveLength(3); - expect(tabs.at(0).props().isTabSelected).toEqual(true); - expect(tabs.at(1).props().isTabSelected).toEqual(false); - expect(tabs.at(2).props().isTabSelected).toEqual(false); - }); - }); - }); - - describe("When a TabTitle has click event", () => { - it("does nothing if triggered by keydown", () => { - const wrapper = render(); - act(() => { - wrapper - .find(TabTitle) - .at(1) - .props() - .onClick(mockEvent("keydown", "uniqueid2")); - }); - wrapper.update(); - expect(wrapper.find(Tab).at(1).props().isTabSelected).toEqual(false); - }); - - it("updates to make the associated Tab visible", () => { - const wrapper = render(); - act(() => { - wrapper - .find(TabTitle) - .at(1) - .props() - .onClick(mockEvent("click", "uniqueid2")); - }); - wrapper.update(); - expect(wrapper.find(Tab).at(1).props().isTabSelected).toEqual(true); - }); - - it('calls the "onTabChange" callback if one is passed on click of a TabTitle', () => { - const onTabChange = jest.fn(); - const wrapper = render({ onTabChange }); - act(() => { - wrapper - .find(TabTitle) - .at(1) - .props() - .onClick(mockEvent("click", "uniqueid2")); - }); - wrapper.update(); - expect(onTabChange).toHaveBeenCalledTimes(1); - expect(onTabChange).toHaveBeenCalledWith("uniqueid2"); - }); - - it("updates the selected tab when a new id is passed via the 'selectedTabId' prop", () => { - const wrapper = render({ selectedTabId: "uniqueid1" }); - wrapper.setProps({ selectedTabId: "uniqueid2" }); - wrapper.update(); - expect(wrapper.find(Tab).at(1).props().isTabSelected).toEqual(true); - wrapper.setProps({ selectedTabId: "uniqueid1" }); - wrapper.update(); - expect(wrapper.find(Tab).at(0).props().isTabSelected).toEqual(true); - }); - - it("blurs the selected TabTitle when a new id is passed via the selectedTabId prop", () => { - const container = document.createElement("div"); - container.id = "container"; - document.body.appendChild(container); - - const wrapper = render( - { selectedTabId: "uniqueid1" }, - { - attachTo: document.querySelector("#container") as HTMLElement, - } - ); - - act(() => { - (wrapper - .find(StyledTabTitleButton) - .at(0) - .getDOMNode() as HTMLElement).focus(); - }); - - expect(document.activeElement).toBe( - wrapper.find(StyledTabTitleButton).at(0).getDOMNode() - ); - - wrapper.setProps({ selectedTabId: "uniqueid2" }); - wrapper.update(); - - expect(document.activeElement).not.toBe( - wrapper.find(StyledTabTitleButton).at(0).getDOMNode() - ); - - wrapper.detach(); - document.body.removeChild(container); - }); - - it('only calls the "onTabChange" callback when visible tabId does not match new tabId', () => { - const onTabChange = jest.fn(); - const wrapper = render({ onTabChange, selectedTabId: "uniqueid1" }); - act(() => { - wrapper - .find(TabTitle) - .at(1) - .props() - .onClick(mockEvent("click", "uniqueid2")); - }); - wrapper.update(); - wrapper.setProps({ selectedTabId: "uniqueid2" }); - wrapper.update(); - expect(onTabChange).toHaveBeenCalledTimes(1); - }); - }); - - describe("When a key is pressed on TabTitle", () => { - let container: HTMLDivElement | null; - let wrapper: ReactWrapper; - - beforeEach(() => { - container = document.createElement("div"); - container.id = "enzymeContainer"; - document.body.appendChild(container); - }); - - afterEach(() => { - if (container && container.parentNode) { - container.parentNode.removeChild(container); - } - - container = null; - }); - - const getTabNode = (index: number): HTMLButtonElement => - wrapper - .find(StyledTabTitleButton) - .at(index) - .getDOMNode() as HTMLButtonElement; - - describe('and the component has position "top" (default)', () => { - it.each([0, 1, 2])( - "focuses the next TabTitle when the right key is pressed", - (index) => { - wrapper = render({}, { attachTo: container }); - act(() => { - simulate.keydown.pressArrowRight(wrapper.find(TabTitle).at(index)); - }); - wrapper.update(); - const newIndex = index === 2 ? 0 : index + 1; - expect(getTabNode(newIndex)).toEqual(document.activeElement); - } - ); - - it.each([0, 2, 1])( - "focuses the previous TabTitle when the left key is pressed", - (index) => { - wrapper = render({}, { attachTo: container }); - act(() => { - simulate.keydown.pressArrowLeft(wrapper.find(TabTitle).at(index)); - }); - wrapper.update(); - const newIndex = index === 0 ? 2 : index - 1; - expect(getTabNode(newIndex)).toEqual(document.activeElement); - } - ); - }); - - describe('and the component has position "left"', () => { - it.each([0, 1, 2])( - "focuses the next TabTitle when the down key is pressed", - (index) => { - wrapper = render({ position: "left" }, { attachTo: container }); - act(() => { - simulate.keydown.pressArrowDown(wrapper.find(TabTitle).at(index)); - }); - wrapper.update(); - const newIndex = index === 2 ? 0 : index + 1; - expect(getTabNode(newIndex)).toEqual(document.activeElement); - } - ); - - it.each([0, 2, 1])( - "focuses the previous TabTitle when the up key is pressed", - (index) => { - wrapper = render({ position: "left" }, { attachTo: container }); - act(() => { - simulate.keydown.pressArrowUp(wrapper.find(TabTitle).at(index)); - }); - wrapper.update(); - const newIndex = index === 0 ? 2 : index - 1; - expect(getTabNode(newIndex)).toEqual(document.activeElement); - } - ); - }); - - it.each([0, 1, 2])( - "activates the Tab when the enter key is pressed", - (index) => { - wrapper = render({}, { attachTo: container }); - act(() => { - // Click event is used because thats the event handler that is used in the component - // In the browser the click event is fired when the enter key is pressed - // Enzyme does not simulate this behaviour correctly - wrapper.find(StyledTabTitleButton).at(index).simulate("click"); - }); - wrapper.update(); - expect(wrapper.find(Tab).at(index).props().isTabSelected).toEqual(true); - } - ); - - it.each([0, 1, 2])("does nothing if key is not an arrow key", (index) => { - wrapper = render({}, { attachTo: container }); - getTabNode(0).focus(); - act(() => { - simulate.keydown.pressD(wrapper.find(TabTitle).at(index)); - }); - wrapper.update(); - expect(getTabNode(0)).toEqual(document.activeElement); - }); - }); - - describe("when in Drawer sidebar", () => { - let container: HTMLElement | null; - let wrapper: ReactWrapper; - - beforeEach(() => { - container = document.createElement("div"); - container.id = "enzymeContainer"; - document.body.appendChild(container); - - wrapper = mount( - - - - - - - , - { - attachTo: document.getElementById("enzymeContainer"), - } - ); - }); - - afterEach(() => { - if (container?.parentNode) { - container.parentNode.removeChild(container); - } - - container = null; - - wrapper?.unmount(); - }); - - it.each([0, 1, 2])( - "updates to make the next Tab visible when the down key is pressed", - (index) => { - act(() => { - simulate.keydown.pressArrowDown(wrapper.find(TabTitle).at(index)); - }); - wrapper.update(); - const newIndex = index === 2 ? 0 : index + 1; - - expect( - wrapper.find(StyledTabTitleButton).at(newIndex).getDOMNode() - ).toBeFocused(); - } - ); - - it.each([0, 1, 2])( - "updates to make the next Tab visible when the up key is pressed", - (index) => { - act(() => { - simulate.keydown.pressArrowUp(wrapper.find(TabTitle).at(index)); - }); - wrapper.update(); - const newIndex = index === 0 ? 2 : index - 1; - - expect( - wrapper.find(StyledTabTitleButton).at(newIndex).getDOMNode() - ).toBeFocused(); - } - ); - }); -}); - -describe("With one Tab", () => { - it("renders as the visible tab", () => { - const tab = mount( - - -
- - - ).find(Tab); - - expect(tab.props().isTabSelected).toEqual(true); - expect(tab.props().title).toEqual("Tab Title 1"); - expect(tab.props().tabId).toEqual("uniqueid1"); - }); -}); - -describe("Validation", () => { - const updateProps = ( - wrapper: ReactWrapper, - props: MockWrapperProps - ) => { - wrapper.setProps({ - errors: { ...wrapper.props().errors, ...props.errors }, - warnings: { ...wrapper.props().warnings, ...props.warnings }, - infos: { ...wrapper.props().infos, ...props.infos }, - }); - wrapper.update(); - }; - - describe("When a Tab child has an error", () => { - it('sets "tabHasError" to false when a Tab has no errors', () => { - const tabTitle = mount().find(TabTitle); - - expect(tabTitle.at(0).props().error).toEqual(false); - expect(tabTitle.at(1).props().error).toEqual(false); - }); - - it('sets "tabHasError" to true when a Tab has errors', () => { - const tabTitle = mount().find( - TabTitle - ); - - expect(tabTitle.at(0).props().error).toEqual(true); - expect(tabTitle.at(1).props().error).toEqual(false); - }); - - it('sets "tabHasError" to true for any Tab that has an error', () => { - const tabTitle = mount( - - ).find(TabTitle); - - expect(tabTitle.at(0).props().error).toEqual(true); - expect(tabTitle.at(1).props().error).toEqual(true); - }); - - it('maintains "tabHasError" status when Tab children update', () => { - const wrapper = mount( - - ); - updateProps(wrapper, { errors: { two: true, three: false } }); - let tabTitle = wrapper.find(TabTitle); - expect(tabTitle.at(0).props().error).toEqual(true); - expect(tabTitle.at(1).props().error).toEqual(false); - updateProps(wrapper, { errors: { one: false, two: false } }); - tabTitle = wrapper.find(TabTitle); - expect(tabTitle.at(0).props().error).toEqual(false); - }); - - it('does not set warnings and infos if "tabHasErrors" is true', () => { - const tabTitle = mount( - - ).find(TabTitle); - - expect(tabTitle.at(0).props().error).toEqual(true); - expect(tabTitle.at(1).props().error).toEqual(true); - expect(tabTitle.at(0).props().warning).toEqual(false); - expect(tabTitle.at(1).props().warning).toEqual(false); - expect(tabTitle.at(0).props().info).toEqual(false); - expect(tabTitle.at(1).props().info).toEqual(false); - }); - }); - - describe("When a Tab child has an error and a warning", () => { - it('sets "tabHasWarning" to false when a Tab has no warnings', () => { - const tabTitle = mount().find(TabTitle); - - expect(tabTitle.at(0).props().warning).toEqual(false); - expect(tabTitle.at(1).props().warning).toEqual(false); - }); - - it('does not set "tabHasWarning" when "tabHasError" is true', () => { - const tabTitle = mount( - - ).find(TabTitle); - - expect(tabTitle.at(0).props().error).toEqual(true); - expect(tabTitle.at(0).props().warning).toEqual(false); - }); - }); - - describe("When a Tab child has a warning and no errors", () => { - it('sets "tabHasWarning" is true and "tabHasError" is falsy', () => { - const tabTitle = mount().find( - TabTitle - ); - - expect(tabTitle.at(0).props().warning).toEqual(true); - expect(tabTitle.at(1).props().warning).toEqual(false); - }); - - it('sets "tabHasWarning" for each Tab that has warning and "tabHasError" is falsy', () => { - const tabTitle = mount( - - ).find(TabTitle); - - expect(tabTitle.at(0).props().warning).toEqual(true); - expect(tabTitle.at(1).props().warning).toEqual(true); - }); - - it('maintains "tabHasWarning" status when Tab children update', () => { - const wrapper = mount( - - ); - updateProps(wrapper, { warnings: { two: true, three: false } }); - let tabTitle = wrapper.find(TabTitle); - expect(tabTitle.at(0).props().warning).toEqual(true); - expect(tabTitle.at(1).props().warning).toEqual(false); - updateProps(wrapper, { warnings: { one: false, two: false } }); - tabTitle = wrapper.find(TabTitle); - expect(tabTitle.at(0).props().warning).toEqual(false); - }); - }); - - describe("When a Tab child has an info and no errors or warnings", () => { - it('sets "tabHasWarning" is true and "tabHasError" is falsy', () => { - const tabTitle = mount().find( - TabTitle - ); - - expect(tabTitle.at(0).props().info).toEqual(true); - expect(tabTitle.at(1).props().info).toEqual(false); - }); - - it('sets "tabHasInfo" for each Tab that has info and "tabHasError" and "tabHasWarning" are falsy', () => { - const tabTitle = mount( - - ).find(TabTitle); - - expect(tabTitle.at(0).props().info).toEqual(true); - expect(tabTitle.at(1).props().info).toEqual(true); - }); - - it('maintains "tabHasInfo" status when Tab children update', () => { - const wrapper = mount(); - updateProps(wrapper, { infos: { two: true, three: false } }); - let tabTitle = wrapper.find(TabTitle); - expect(tabTitle.at(0).props().info).toEqual(true); - expect(tabTitle.at(1).props().info).toEqual(false); - updateProps(wrapper, { infos: { one: false, two: false } }); - tabTitle = wrapper.find(TabTitle); - expect(tabTitle.at(0).props().info).toEqual(false); - }); - }); - - describe("custom targeting", () => { - it("supports overriding the targeted content", () => { - const wrapper = mount( - - - - TabContent - - - - ); - act(() => { - wrapper.find(TabTitle).props().onClick(mockEvent("click", "uniqueid1")); - }); - expect(wrapper.find(Tab).exists()).toEqual(false); - }); - }); - - describe("validation status overrides", () => { - it('sets "tabHasError" to true when override is set', () => { - const tabTitle = mount( - - ).find(TabTitle); - - expect(tabTitle.at(0).props().error).toEqual(true); - expect(tabTitle.at(1).props().error).toEqual(false); - }); - - it('sets "tabHasWarning" to true when override is set', () => { - const tabTitle = mount( - - ).find(TabTitle); - - expect(tabTitle.at(0).props().warning).toEqual(true); - expect(tabTitle.at(1).props().warning).toEqual(false); - }); - - it('sets "tabHasInfo" to true when override is set', () => { - const tabTitle = mount( - - ).find(TabTitle); - - expect(tabTitle.at(0).props().info).toEqual(true); - expect(tabTitle.at(1).props().info).toEqual(false); - }); - }); - - describe("Keyboard behaviour", () => { - let container: HTMLElement | null; - let wrapper: ReactWrapper; - const tabTitles = ["tab-1", "tab-2", "tab-3"]; - - const ConditionalChildrenMock = () => { - const [showAllTabs, setShowAllTabs] = React.useState(true); - - const generateTab = (tabTitle: string) => ( - - {tabTitle} - - ); - - return ( - <> - - - {!showAllTabs && generateTab(tabTitles[0])} - {showAllTabs && tabTitles.map((tabTitle) => generateTab(tabTitle))} - - - ); - }; - - beforeEach(() => { - container = document.createElement("div"); - container.id = "enzymeContainer"; - document.body.appendChild(container); - - wrapper = mount(, { - attachTo: document.getElementById("enzymeContainer"), - }); - }); - - afterEach(() => { - if (container?.parentNode) { - container.parentNode.removeChild(container); - } - - container = null; - }); - - const runFocusExpectations = (keyDown: string, array: number[]) => - array.forEach((index) => { - const child = wrapper.update().find(StyledTabTitleButton).at(index); - expect(child.getDOMNode()).toBeFocused(); - simulate.keydown[keyDown](child); - }); - - const toggleChildren = () => { - act(() => { - wrapper.find("#foo").simulate("click"); - }); - - expect(wrapper.update().find(StyledTabTitleButton).length).toEqual(1); - - act(() => { - wrapper.find("#foo").simulate("click"); - }); - - expect(wrapper.update().find(StyledTabTitleButton).length).toEqual(3); - }; - - it("is consistent when navigating with the arrow keys and the composition of the children changes", () => { - (wrapper - .find(StyledTabTitleButton) - .first() - .getDOMNode() as HTMLElement).focus(); - - runFocusExpectations("pressArrowLeft", [0, 2, 1, 0, 2]); - - toggleChildren(); - - (wrapper - .find(StyledTabTitleButton) - .first() - .getDOMNode() as HTMLElement).focus(); - - runFocusExpectations("pressArrowLeft", [0, 2, 1, 0, 2]); - - toggleChildren(); - - (wrapper - .find(StyledTabTitleButton) - .first() - .getDOMNode() as HTMLElement).focus(); - - runFocusExpectations("pressArrowRight", [0, 1, 2, 0]); - }); - }); -}); - -describe("tags", () => { - describe("on component", () => { - const wrapper = shallow( - - - - ).find(StyledTabs); - - it("include correct component, element and role data tags", () => { - rootTagTest(wrapper, "tabs", "bar", "baz"); - }); - }); - - it("when child Tab has `data-role` prop set, renders corresponding tab title with that `data-role`", () => { - const dataRole = "foobar"; - const wrapper = mount( - - - Content for Tab 1 - - - ); - expect(wrapper.find(TabTitle).prop("data-role")).toEqual(dataRole); - }); - - describe("when children of Tab have validation failures", () => { - const MockComponent = ({ - show = true, - error, - warning, - info, - }: ValidationProps & { show?: boolean }) => ( - - - {show && ( - {}} - error={error} - warning={warning} - info={info} - /> - )} - - - ); - - it.each(["error", "warning", "info"])( - "any %s failure in a child component is correctly reported in the TabTitle", - (validation) => { - const validationProp = { [validation]: true }; - const wrapper = mount(); - - expect(wrapper.find(ValidationIcon).exists()).toBe(true); - - wrapper.setProps({ show: false }); - wrapper.update(); - expect(wrapper.update().find(ValidationIcon).exists()).toBe(false); - } - ); - }); - - describe.each(["error", "warning", "info"])( - "showValidationsSummary", - (validation) => { - it(`passes the ${validation} validation failures from the child inputs to the Tab's title when they are strings`, () => { - const message = mount( - - - {}} - {...{ [validation]: validation }} - /> - {}} - {...{ [validation]: validation }} - /> - {}} - {...{ [validation]: validation }} - /> - - - ) - .find(TabTitle) - .prop(`${validation}Message`); - - expect(message).toEqual( - `• ${validation}\n• ${validation}\n• ${validation}` - ); - }); - - it(`does not pass the ${validation} validation failures from the child inputs to the Tab's title when they are not strings`, () => { - const message = mount( - - - {}} - {...{ [validation]: validation }} - /> - {}} - {...{ [validation]: validation }} - /> - {}} - {...{ [validation]: true }} - /> - - - ) - .find(TabTitle) - .prop(`${validation}Message`); - - expect(message).toEqual(`• ${validation}\n• ${validation}`); - }); - - it(`does not add a "•" when there is only one string ${validation} validation failure`, () => { - const message = mount( - - - {}} - {...{ [validation]: validation }} - /> - - - ) - .find(TabTitle) - .prop(`${validation}Message`); - - expect(message).toEqual(validation); - }); - - it(`passes the ${validation}Message if there is only boolean ${validation} validation failures`, () => { - const message = mount( - - - {}} - {...{ [validation]: true }} - /> - {}} - {...{ [validation]: true }} - /> - {}} - {...{ [validation]: true }} - /> - - - ) - .find(TabTitle) - .prop(`${validation}Message`); - - expect(message).toEqual(`${validation} message`); - }); - } - ); -}); diff --git a/src/components/tabs/tabs.test.tsx b/src/components/tabs/tabs.test.tsx new file mode 100644 index 0000000000..39b9b16534 --- /dev/null +++ b/src/components/tabs/tabs.test.tsx @@ -0,0 +1,1525 @@ +import React, { useState } from "react"; +import { render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Tabs, Tab } from "."; +import { StyledTabsHeaderWrapper } from "./__internal__/tabs-header/tabs-header.style"; +import StyledTab from "./tab/tab.style"; +import { testStyledSystemMargin } from "../../__spec_helper__/__internal__/test-utils"; +import Drawer from "../drawer"; +import Textbox from "../textbox"; +import NumeralDate from "../numeral-date"; + +testStyledSystemMargin((props) => ( + + + TabContent + + +)); + +testStyledSystemMargin((props) => ( + + + TabContent + + +)); + +test("should not throw an error when rendered with NumeralDate as a child", () => { + expect(() => { + render( + + + + {}} /> + + + ); + }).not.toThrow(); +}); + +test("should throw an error if rendered with `headerWidth` prop and `position` prop set to `top`", () => { + const consoleSpy = jest + .spyOn(global.console, "error") + .mockImplementation(() => {}); + + render( + + + TabContent + + + ); + + expect(consoleSpy).toHaveBeenCalledWith( + "Invalid usage of prop headerWidth in Tabs. The headerWidth can be used only if position is set to left" + ); + + consoleSpy.mockRestore(); +}); + +test("passes the `className` prop down to the element", () => { + render( + + + + ); + + expect(screen.getByTestId("tabs")).toHaveClass( + "custom-class-1", + "custom-class-2" + ); +}); + +test("the `selectedTabId` prop determines which child `Tab` is displayed", () => { + render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + + expect(screen.getByText("Content for tab 1")).not.toBeVisible(); + expect(screen.getByText("Content for tab 2")).toBeVisible(); + expect(screen.getByText("Content for tab 3")).not.toBeVisible(); +}); + +test("when the `renderHiddenTabs` prop is false, only the currently visible tab is rendered in the DOM", () => { + render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + + expect(screen.queryByText("Content for tab 1")).not.toBeInTheDocument(); + expect(screen.getByText("Content for tab 2")).toBeVisible(); + expect(screen.queryByText("Content for tab 3")).not.toBeInTheDocument(); +}); + +test.each(["error", "warning", "info"] as const)( + "when `renderHiddenTabs` is false, adds the correct %s state to the tab header only in the visible tab when the validation prop is set", + (validation) => { + render( + + + {}} {...{ [validation]: true }} /> + + + {}} {...{ [validation]: true }} /> + + + ); + + expect( + within(screen.getByRole("tab", { name: "Tab Title 1" })).getByTestId( + `icon-${validation}` + ) + ).toBeVisible(); + expect( + within(screen.getByRole("tab", { name: "Tab Title 2" })).queryByTestId( + `icon-${validation}` + ) + ).not.toBeInTheDocument(); + } +); + +test("when the `renderHiddenTabs` prop is true, all tabs are rendered in the DOM with only the first visible", () => { + render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + + expect(screen.getByText("Content for tab 1")).toBeVisible(); + expect(screen.getByText("Content for tab 2")).not.toBeVisible(); + expect(screen.getByText("Content for tab 3")).not.toBeVisible(); +}); + +test("when a tab title is clicked, the associated tab becomes the visible one", async () => { + const user = userEvent.setup(); + render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + expect(screen.getByText("Content for tab 1")).toBeVisible(); + expect(screen.getByText("Content for tab 2")).not.toBeVisible(); + + await user.click(screen.getByRole("tab", { name: "Tab 2" })); + expect(screen.getByText("Content for tab 1")).not.toBeVisible(); + expect(screen.getByText("Content for tab 2")).toBeVisible(); +}); + +test("when a tab title is clicked, the `onTabChange` callback prop gets called with the tab id", async () => { + const user = userEvent.setup(); + const onTabChange = jest.fn(); + render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + + await user.click(screen.getByRole("tab", { name: "Tab 2" })); + expect(onTabChange).toHaveBeenCalledTimes(1); + expect(onTabChange).toHaveBeenCalledWith("tab-2"); +}); + +test("updates the selected tab when the `selectedTabId` prop is updated", () => { + const { rerender } = render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + expect(screen.getByText("Content for tab 1")).toBeVisible(); + expect(screen.getByText("Content for tab 2")).not.toBeVisible(); + + rerender( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + expect(screen.getByText("Content for tab 1")).not.toBeVisible(); + expect(screen.getByText("Content for tab 2")).toBeVisible(); +}); + +test("blurs the previously-selected tab title when the `selectedTabId` prop is updated", () => { + const { rerender } = render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + screen.getByRole("tab", { name: "Tab 1" }).focus(); + + rerender( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + expect(screen.getByRole("tab", { name: "Tab 1" })).not.toHaveFocus(); +}); + +test("does not call the `onTabChange` callback when rerendered with the selected tab as the new `selectedTabId", async () => { + const user = userEvent.setup(); + const onTabChange = jest.fn(); + const { rerender } = render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + await user.click(screen.getByRole("tab", { name: "Tab 2" })); + expect(onTabChange).toHaveBeenCalledTimes(1); + + rerender( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + expect(onTabChange).toHaveBeenCalledTimes(1); +}); + +test("when the position is `top` (the default), pressing the right arrow key focuses the next tab title", async () => { + const user = userEvent.setup(); + render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + + screen.getByRole("tab", { name: "Tab 1" }).focus(); + + await user.keyboard("{ArrowRight}"); + expect(screen.getByRole("tab", { name: "Tab 2" })).toHaveFocus(); + + await user.keyboard("{ArrowRight}"); + expect(screen.getByRole("tab", { name: "Tab 3" })).toHaveFocus(); +}); + +test("when the position is `top` (the default), pressing the right arrow key when focused on the last tab title focuses the first one", async () => { + const user = userEvent.setup(); + render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + + screen.getByRole("tab", { name: "Tab 3" }).focus(); + + await user.keyboard("{ArrowRight}"); + expect(screen.getByRole("tab", { name: "Tab 1" })).toHaveFocus(); +}); + +test("when the position is `top` (the default), pressing the left arrow key focuses the previous tab title", async () => { + const user = userEvent.setup(); + render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + + screen.getByRole("tab", { name: "Tab 3" }).focus(); + + await user.keyboard("{ArrowLeft}"); + expect(screen.getByRole("tab", { name: "Tab 2" })).toHaveFocus(); + + await user.keyboard("{ArrowLeft}"); + expect(screen.getByRole("tab", { name: "Tab 1" })).toHaveFocus(); +}); + +test("when the position is `top` (the default), pressing the left arrow key when focused on the first tab title focuses the last one", async () => { + const user = userEvent.setup(); + render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + + screen.getByRole("tab", { name: "Tab 1" }).focus(); + + await user.keyboard("{ArrowLeft}"); + expect(screen.getByRole("tab", { name: "Tab 3" })).toHaveFocus(); +}); + +test("when the position is `left`, pressing the down arrow key focuses the next tab title", async () => { + const user = userEvent.setup(); + render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + + screen.getByRole("tab", { name: "Tab 1" }).focus(); + + await user.keyboard("{ArrowDown}"); + expect(screen.getByRole("tab", { name: "Tab 2" })).toHaveFocus(); + + await user.keyboard("{ArrowDown}"); + expect(screen.getByRole("tab", { name: "Tab 3" })).toHaveFocus(); +}); + +test("when the position is `left`, pressing the down arrow key when focused on the last tab title focuses the first one", async () => { + const user = userEvent.setup(); + render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + + screen.getByRole("tab", { name: "Tab 3" }).focus(); + + await user.keyboard("{ArrowDown}"); + expect(screen.getByRole("tab", { name: "Tab 1" })).toHaveFocus(); +}); + +test("when the position is `left`, pressing the up arrow key focuses the previous tab title", async () => { + const user = userEvent.setup(); + render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + + screen.getByRole("tab", { name: "Tab 3" }).focus(); + + await user.keyboard("{ArrowUp}"); + expect(screen.getByRole("tab", { name: "Tab 2" })).toHaveFocus(); + + await user.keyboard("{ArrowUp}"); + expect(screen.getByRole("tab", { name: "Tab 1" })).toHaveFocus(); +}); + +test("when the position is `left`, pressing the up arrow key when focused on the first tab title focuses the last one", async () => { + const user = userEvent.setup(); + render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + + screen.getByRole("tab", { name: "Tab 1" }).focus(); + + await user.keyboard("{ArrowUp}"); + expect(screen.getByRole("tab", { name: "Tab 3" })).toHaveFocus(); +}); + +test("when the Enter key is pressed on a tab title, the associated tab becomes the visible one", async () => { + const user = userEvent.setup(); + render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + expect(screen.getByText("Content for tab 1")).toBeVisible(); + expect(screen.getByText("Content for tab 2")).not.toBeVisible(); + + screen.getByRole("tab", { name: "Tab 2" }).focus(); + await user.keyboard("{Enter}"); + expect(screen.getByText("Content for tab 1")).not.toBeVisible(); + expect(screen.getByText("Content for tab 2")).toBeVisible(); +}); + +test("when a non-Enter, non-arrow key is pressed on a tab title, neither the visible tab nor the focused tab title updates", async () => { + const user = userEvent.setup(); + render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + expect(screen.getByText("Content for tab 1")).toBeVisible(); + expect(screen.getByText("Content for tab 2")).not.toBeVisible(); + expect(screen.getByText("Content for tab 3")).not.toBeVisible(); + screen.getByRole("tab", { name: "Tab 1" }).focus(); + + await user.keyboard("a"); + expect(screen.getByText("Content for tab 1")).toBeVisible(); + expect(screen.getByText("Content for tab 2")).not.toBeVisible(); + expect(screen.getByText("Content for tab 3")).not.toBeVisible(); + expect(screen.getByRole("tab", { name: "Tab 1" })).toHaveFocus(); +}); + +test("when rendered in a Drawer sidebar, pressing the down arrow key focuses the next tab title", async () => { + const user = userEvent.setup(); + render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + } + > + drawer content + + ); + screen.getByRole("tab", { name: "Tab 1" }).focus(); + + await user.keyboard("{ArrowDown}"); + expect(screen.getByRole("tab", { name: "Tab 2" })).toHaveFocus(); + + await user.keyboard("{ArrowDown}"); + expect(screen.getByRole("tab", { name: "Tab 3" })).toHaveFocus(); +}); + +test("when rendered in a Drawer sidebar`, pressing the down arrow key when focused on the last tab title focuses the first one", async () => { + const user = userEvent.setup(); + render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + } + > + drawer content + + ); + + screen.getByRole("tab", { name: "Tab 3" }).focus(); + + await user.keyboard("{ArrowDown}"); + expect(screen.getByRole("tab", { name: "Tab 1" })).toHaveFocus(); +}); + +test("when rendered in a Drawer sidebar, pressing the up arrow key focuses the previous tab title", async () => { + const user = userEvent.setup(); + render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + } + > + drawer content + + ); + + screen.getByRole("tab", { name: "Tab 3" }).focus(); + + await user.keyboard("{ArrowUp}"); + expect(screen.getByRole("tab", { name: "Tab 2" })).toHaveFocus(); + + await user.keyboard("{ArrowUp}"); + expect(screen.getByRole("tab", { name: "Tab 1" })).toHaveFocus(); +}); + +test("when rendered in a Drawer sidebar, pressing the up arrow key when focused on the first tab title focuses the last one", async () => { + const user = userEvent.setup(); + render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + } + > + drawer content + + ); + + screen.getByRole("tab", { name: "Tab 1" }).focus(); + + await user.keyboard("{ArrowUp}"); + expect(screen.getByRole("tab", { name: "Tab 3" })).toHaveFocus(); +}); + +test("when there is no validation issue in a tab, no validation icon is displayed in the tab title", () => { + render( + + + {}} /> + + + {}} /> + + + ); + + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-error" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).queryByTestId( + "icon-error" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-warning" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).queryByTestId( + "icon-warning" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-info" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).queryByTestId( + "icon-info" + ) + ).not.toBeInTheDocument(); +}); + +test("when there is an error in a tab, an error icon is displayed in the corresponding tab title", () => { + render( + + + {}} /> + + + {}} /> + + + ); + + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-error" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).getByTestId("icon-error") + ).toBeVisible(); + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-warning" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).queryByTestId( + "icon-warning" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-info" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).queryByTestId( + "icon-info" + ) + ).not.toBeInTheDocument(); +}); + +test("when errors and warnings are both present in a tab, only the error icon is displayed in the corresponding tab title", () => { + render( + + + {}} /> + + + {}} /> + {}} /> + + + ); + + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-error" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).getByTestId("icon-error") + ).toBeVisible(); + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-warning" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).queryByTestId( + "icon-warning" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-info" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).queryByTestId( + "icon-info" + ) + ).not.toBeInTheDocument(); +}); + +test("when errors, warnings and infos are all present in a tab, only the error icon is displayed in the corresponding tab title", () => { + render( + + + {}} /> + + + {}} /> + {}} /> + {}} /> + + + ); + + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-error" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).getByTestId("icon-error") + ).toBeVisible(); + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-warning" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).queryByTestId( + "icon-warning" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-info" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).queryByTestId( + "icon-info" + ) + ).not.toBeInTheDocument(); +}); + +test("when a tab has warnings and no errors, a warning icon is displayed in the corresponding tab title", () => { + render( + + + {}} /> + + + {}} /> + + + ); + + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-error" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).queryByTestId( + "icon-error" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-warning" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).getByTestId( + "icon-warning" + ) + ).toBeVisible(); + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-info" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).queryByTestId( + "icon-info" + ) + ).not.toBeInTheDocument(); +}); + +test("when a tab has info and no errors or warnings, an info icon is displayed in the corresponding tab title", () => { + render( + + + {}} /> + + + {}} /> + + + ); + + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-error" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).queryByTestId( + "icon-error" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-warning" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).queryByTestId( + "icon-warning" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-info" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).getByTestId("icon-info") + ).toBeVisible(); +}); + +test("an error icon is displayed in the corresponding tab title when specified by the `validationStatusOverride` prop", () => { + render( + + + {}} /> + + + {}} /> + + + ); + + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-error" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).getByTestId("icon-error") + ).toBeVisible(); + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-warning" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).queryByTestId( + "icon-warning" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-info" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).queryByTestId( + "icon-info" + ) + ).not.toBeInTheDocument(); +}); + +test("a warning icon is displayed in the corresponding tab title when specified by the `validationStatusOverride` prop", () => { + render( + + + {}} /> + + + {}} /> + + + ); + + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-error" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).queryByTestId( + "icon-error" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-warning" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).getByTestId( + "icon-warning" + ) + ).toBeVisible(); + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-info" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).queryByTestId( + "icon-info" + ) + ).not.toBeInTheDocument(); +}); + +test("an info icon is displayed in the corresponding tab title when specified by the `validationStatusOverride` prop", () => { + render( + + + {}} /> + + + {}} /> + + + ); + + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-error" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).queryByTestId( + "icon-error" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-warning" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).queryByTestId( + "icon-warning" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 1" })).queryByTestId( + "icon-info" + ) + ).not.toBeInTheDocument(); + expect( + within(screen.getByRole("tab", { name: "Tab 2" })).getByTestId("icon-info") + ).toBeVisible(); +}); + +test("arrow key navigation remains consistent when tab children are added and removed", async () => { + const user = userEvent.setup(); + const WithConditionalChildren = () => { + const [showAllTabs, setShowAllTabs] = useState(true); + + const generateTab = (tabTitle: string) => ( + + {tabTitle} + + ); + + return ( + <> + + + {!showAllTabs && generateTab("tab-1")} + {showAllTabs && + [1, 2, 3].map((tabNumber) => generateTab(`tab-${tabNumber}`))} + + + ); + }; + render(); + + screen.getByRole("tab", { name: "tab-1" }).focus(); + + expect(screen.getByRole("tab", { name: "tab-1" })).toHaveFocus(); + await user.keyboard("{ArrowLeft}"); + expect(screen.getByRole("tab", { name: "tab-3" })).toHaveFocus(); + await user.keyboard("{ArrowLeft}"); + expect(screen.getByRole("tab", { name: "tab-2" })).toHaveFocus(); + await user.keyboard("{ArrowLeft}"); + expect(screen.getByRole("tab", { name: "tab-1" })).toHaveFocus(); + await user.keyboard("{ArrowLeft}"); + expect(screen.getByRole("tab", { name: "tab-3" })).toHaveFocus(); + + await user.click(screen.getByRole("button", { name: "Toggle children" })); + expect(screen.getAllByRole("tab")).toHaveLength(1); + await user.click(screen.getByRole("button", { name: "Toggle children" })); + expect(screen.getAllByRole("tab")).toHaveLength(3); + + screen.getByRole("tab", { name: "tab-1" }).focus(); + + expect(screen.getByRole("tab", { name: "tab-1" })).toHaveFocus(); + await user.keyboard("{ArrowLeft}"); + expect(screen.getByRole("tab", { name: "tab-3" })).toHaveFocus(); + await user.keyboard("{ArrowLeft}"); + expect(screen.getByRole("tab", { name: "tab-2" })).toHaveFocus(); + await user.keyboard("{ArrowLeft}"); + expect(screen.getByRole("tab", { name: "tab-1" })).toHaveFocus(); + await user.keyboard("{ArrowLeft}"); + expect(screen.getByRole("tab", { name: "tab-3" })).toHaveFocus(); + await user.click(screen.getByRole("button", { name: "Toggle children" })); + expect(screen.getAllByRole("tab")).toHaveLength(1); + await user.click(screen.getByRole("button", { name: "Toggle children" })); + expect(screen.getAllByRole("tab")).toHaveLength(3); + + screen.getByRole("tab", { name: "tab-1" }).focus(); + + expect(screen.getByRole("tab", { name: "tab-1" })).toHaveFocus(); + await user.keyboard("{ArrowRight}"); + expect(screen.getByRole("tab", { name: "tab-2" })).toHaveFocus(); + await user.keyboard("{ArrowRight}"); + expect(screen.getByRole("tab", { name: "tab-3" })).toHaveFocus(); + await user.keyboard("{ArrowRight}"); + expect(screen.getByRole("tab", { name: "tab-1" })).toHaveFocus(); +}); + +test("has the expected `data-component` attribute", () => { + render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + + expect(screen.getByTestId("tabs")).toHaveAttribute("data-component", "tabs"); +}); + +test("accepts `data-element` and `data-role` props", () => { + render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + + expect(screen.getByTestId("bar")).toHaveAttribute("data-element", "foo"); +}); + +test("child Tab components accept a `data-role` prop via `titleProps` which is added to the corresponding tab title", () => { + render( + + + + ); + + expect(screen.getByRole("tab")).toHaveAttribute("data-role", "foo"); +}); + +test.each(["error", "warning", "info"])( + "passes the all %s messages from the child inputs to the tooltip in the Tab title when they are strings and the `showValidationsSummary` prop is set", + async (validation) => { + const user = userEvent.setup(); + render( + + + {}} + {...{ [validation]: "first validation message" }} + /> + {}} + {...{ [validation]: "second validation message" }} + /> + {}} + {...{ [validation]: "third validation message" }} + /> + + + ); + + await user.hover( + within(screen.getByRole("tab", { name: "Test" })).getByTestId( + `icon-${validation}` + ) + ); + expect(screen.getByRole("tooltip")).toHaveTextContent( + "• first validation message • second validation message • third validation message" + ); + } +); + +test.each(["error", "warning", "info"])( + "leaves out boolean values from the %s messages in child inputs from the tooltip in the Tab title when the `showValidationsSummary` prop is set", + async (validation) => { + const user = userEvent.setup(); + render( + + + {}} + {...{ [validation]: "first validation message" }} + /> + {}} {...{ [validation]: true }} /> + {}} + {...{ [validation]: "third validation message" }} + /> + + + ); + + await user.hover( + within(screen.getByRole("tab", { name: "Test" })).getByTestId( + `icon-${validation}` + ) + ); + expect(screen.getByRole("tooltip")).toHaveTextContent( + "• first validation message • third validation message" + ); + } +); + +test.each(["error", "warning", "info"])( + "passes the %s message from the child inputs to the tooltip in the Tab title with no `•` character when there is only one such message, and the `showValidationsSummary` prop is set", + async (validation) => { + const user = userEvent.setup(); + render( + + + {}} + {...{ [validation]: "first validation message" }} + /> + + + ); + + await user.hover( + within(screen.getByRole("tab", { name: "Test" })).getByTestId( + `icon-${validation}` + ) + ); + expect(screen.getByRole("tooltip")).toHaveTextContent( + "first validation message" + ); + } +); + +test.each(["error", "warning", "info"])( + "uses just the `%sMessage` prop as the tooltip message when all child validation props are boolean and the `showValidationsSummary` prop is set", + async (validation) => { + const user = userEvent.setup(); + render( + + + {}} {...{ [validation]: true }} /> + {}} {...{ [validation]: true }} /> + {}} {...{ [validation]: true }} /> + + + ); + + await user.hover( + within(screen.getByRole("tab", { name: "Test" })).getByTestId( + `icon-${validation}` + ) + ); + expect(screen.getByRole("tooltip")).toHaveTextContent("a single message"); + } +); + +test.each(["error", "warning", "info"])( + "uses just the `%sMessage` prop as the tooltip message when the `showValidationsSummary` prop is not set", + async (validation) => { + const user = userEvent.setup(); + render( + + + {}} + {...{ [validation]: "first validation message" }} + /> + {}} + {...{ [validation]: "second validation message" }} + /> + {}} + {...{ [validation]: "third validation message" }} + /> + + + ); + + await user.hover( + within(screen.getByRole("tab", { name: "Test" })).getByTestId( + `icon-${validation}` + ) + ); + expect(screen.getByRole("tooltip")).toHaveTextContent("a single message"); + } +); + +// coverage - headerWidth prop tested in Chromatic +test.each(["35%", "100px", "5em"])( + "should render tab header with the correct width when the `headerWidth` prop is provided and the `position` prop is `left`", + (headerWidth) => { + render( + + + TabContent + + + ); + + expect(screen.getByTestId("tabs")).toHaveStyleRule("width", headerWidth, { + modifier: `${StyledTabsHeaderWrapper}`, + }); + expect(screen.getByTestId("tabs")).toHaveStyleRule( + "width", + `calc(100% - ${headerWidth})`, + { + modifier: `${StyledTab}`, + } + ); + } +); + +// coverage +test("leaves the same tab content visible when the already-selected tab is clicked", async () => { + const user = userEvent.setup(); + render( + + + Content for tab 1 + + + Content for tab 2 + + + Content for tab 3 + + + ); + + await user.click(screen.getByRole("tab", { name: "Tab 1" })); + expect(screen.getByText("Content for tab 1")).toBeVisible(); + expect(screen.getByText("Content for tab 2")).not.toBeVisible(); +});