diff --git a/packages/lib/jest.config.js b/packages/lib/jest.config.js index 6ba0f4d7de..9b9356956f 100644 --- a/packages/lib/jest.config.js +++ b/packages/lib/jest.config.js @@ -1,5 +1,10 @@ module.exports = { collectCoverage: true, + coveragePathIgnorePatterns: [ + "utils.ts", + "index.ts", + ".*Context\\.tsx$", // Is deprecated and will be removed in the future + ], moduleNameMapper: { "\\.(css|less|scss|sass)$": "identity-obj-proxy", "\\.(svg)$": "/test/mocks/svgMock.js", diff --git a/packages/lib/src/bulleted-list/BulletedList.test.tsx b/packages/lib/src/bulleted-list/BulletedList.test.tsx index 3ae6170d1f..0ac2c5fe7b 100644 --- a/packages/lib/src/bulleted-list/BulletedList.test.tsx +++ b/packages/lib/src/bulleted-list/BulletedList.test.tsx @@ -1,5 +1,6 @@ import { render } from "@testing-library/react"; import DxcBulletedList from "./BulletedList"; +import DxcIcon from "../icon/Icon"; describe("Bulleted list component tests", () => { test("The component renders properly", () => { @@ -14,4 +15,49 @@ describe("Bulleted list component tests", () => { expect(getByText("Usage")).toBeTruthy(); expect(getByText("Specifications")).toBeTruthy(); }); + test("The component renders default (disc) bullets", () => { + const { container } = render( + + Item 1 + + ); + expect(container.querySelector("ul")).toBeTruthy(); + expect(container.querySelector("div")).toBeTruthy(); + }); + + test("The component renders number bullets", () => { + const { container, getByText } = render( + + Numbered Item + + ); + expect(container.querySelector("ol")).toBeTruthy(); + expect(getByText("1.")).toBeTruthy(); + }); + + test("The component renders icon bullets with icon string", () => { + const { container } = render( + + Icon Item + + ); + expect(container.querySelector("span")).toBeTruthy(); + expect(container.innerHTML).toContain("Icon Item"); + }); + + test("The component renders icon bullets with React element icon", () => { + const icon = ( + + + + + ); + const { container } = render( + + Icon React Element + + ); + expect(container.querySelector("svg")).toBeTruthy(); + expect(container.innerHTML).toContain("Icon React Element"); + }); }); diff --git a/packages/lib/src/data-grid/DataGrid.stories.tsx b/packages/lib/src/data-grid/DataGrid.stories.tsx index d4e4d8708b..e78d010fc8 100644 --- a/packages/lib/src/data-grid/DataGrid.stories.tsx +++ b/packages/lib/src/data-grid/DataGrid.stories.tsx @@ -8,7 +8,7 @@ import { disabledRules } from "../../test/accessibility/rules/specific/data-grid import preview from "../../.storybook/preview"; import { userEvent, within } from "@storybook/test"; import DxcBadge from "../badge/Badge"; -import { Action } from "../table/types"; +import { ActionsCellPropsType } from "../table/types"; import { Meta, StoryObj } from "@storybook/react"; import { isKeyOfRow } from "./utils"; @@ -27,7 +27,7 @@ export default { }, } as Meta; -const actions: Action = [ +const actions: ActionsCellPropsType["actions"] = [ { title: "icon", onClick: (value?) => { diff --git a/packages/lib/src/data-grid/DataGrid.test.tsx b/packages/lib/src/data-grid/DataGrid.test.tsx index 61a0a8376f..337a79916a 100644 --- a/packages/lib/src/data-grid/DataGrid.test.tsx +++ b/packages/lib/src/data-grid/DataGrid.test.tsx @@ -1,4 +1,4 @@ -import { render } from "@testing-library/react"; +import { fireEvent, render, waitFor } from "@testing-library/react"; import DxcDataGrid from "./DataGrid"; import { GridColumn, HierarchyGridRow } from "./types"; @@ -13,7 +13,7 @@ const columns: GridColumn[] = [ }, { key: "complete", - label: " % Complete", + label: "% Complete", resizable: true, sortable: true, draggable: true, @@ -48,6 +48,154 @@ const expandableRows = [ }, ]; +const hierarchyRows: HierarchyGridRow[] = [ + { + name: "Root Node 1", + value: "1", + id: "a", + childRows: [ + { + name: "Child Node 1.1", + value: "1.1", + id: "aa", + childRows: [ + { + name: "Grandchild Node 1.1.1", + value: "1.1.1", + id: "aaa", + }, + { + name: "Grandchild Node 1.1.2", + value: "1.1.2", + id: "aab", + }, + ], + }, + { + name: "Child Node 1.2", + value: "1.2", + id: "ab", + }, + ], + }, + { + name: "Root Node 2", + value: "2", + id: "b", + childRows: [ + { + name: "Child Node 2.1", + value: "2.1", + id: "ba", + childRows: [ + { + name: "Grandchild Node 2.1.1", + value: "2.1.1", + id: "baa", + }, + ], + }, + { + name: "Child Node 2.2", + value: "2.2", + id: "bb", + }, + { + name: "Child Node 2.3", + value: "2.3", + id: "bc", + }, + ], + }, + { + name: "Root Node 3", + value: "3", + id: "c", + childRows: [ + { + name: "Child Node 3.1", + value: "3.1", + id: "cc", + childRows: [ + { + name: "Grandchild Node 3.1.1", + value: "3.1.1", + id: "ccc", + }, + { + name: "Grandchild Node 3.1.2", + value: "3.1.2", + id: "ccd", + }, + ], + }, + { + name: "Child Node 3.2", + value: "3.2", + id: "cd", + }, + ], + }, + { + name: "Root Node 4", + value: "4", + id: "d", + childRows: [ + { + name: "Child Node 4.1", + value: "4.1", + id: "da", + childRows: [ + { + name: "Grandchild Node 4.1.1", + value: "4.1.1", + id: "daa", + }, + ], + }, + { + name: "Child Node 4.2", + value: "4.2", + id: "dd", + }, + { + name: "Child Node 4.3", + value: "4.3", + id: "de", + }, + ], + }, + { + name: "Root Node 5", + value: "5", + id: "d", + childRows: [ + { + name: "Child Node 5.1", + value: "5.1", + id: "da", + childRows: [ + { + name: "Grandchild Node 5.1.1", + value: "5.1.1", + id: "daa", + }, + ], + }, + { + name: "Child Node 5.2", + value: "5.2", + id: "dd", + }, + { + name: "Child Node 5.3", + value: "5.3", + id: "de", + }, + ], + }, +] as HierarchyGridRow[]; + describe("Data grid component tests", () => { beforeAll(() => { (global as any).CSS = { @@ -55,24 +203,79 @@ describe("Data grid component tests", () => { }; window.HTMLElement.prototype.scrollIntoView = jest.fn; }); + test("Renders with correct content", async () => { - const { getByText, getAllByRole } = await render( - - ); + const { getByText, getAllByRole } = render(); expect(getByText("46")).toBeTruthy(); const rows = getAllByRole("row"); expect(rows.length).toBe(5); }); - // test("Content is sorted correctly", async () => { - // const { getByText, getAllByRole } = await render(); - // expect(getByText("% Complete")).toBeTruthy(); - // const headerCell = screen.getAllByRole("columnheader")[1]; - // expect(getAllByRole("gridcell")[0].textContent).toBe("1"); - // expect(headerCell.textContent).toBe(" % Complete"); - // await fireEvent.click(headerCell); - // expect(headerCell.getAttribute("aria-sort")).toBe("ascending"); - // expect(getByText("5")).toBeTruthy(); - // // await waitFor(() => expect(getAllByRole("gridcell")[0].textContent).toBe("4")); - // //waitFor(() => expect(getAllByRole("gridcell").length).toBe(8)); - // }); + + test("Renders hierarchy rows", () => { + const onSelectRows = jest.fn(); + const selectedRows = new Set(); + const { getAllByRole } = render( + + ); + const rows = getAllByRole("row"); + expect(rows.length).toBe(5); + }); + + test("Renders column headers", () => { + const { getByText } = render(); + expect(getByText("ID")).toBeTruthy(); + expect(getByText("% Complete")).toBeTruthy(); + }); + + test("Expands and collapses a row to show custom content", async () => { + const { getAllByRole, getByText, queryByText } = render( + + ); + const buttons = getAllByRole("button"); + buttons[0] && fireEvent.click(buttons[0]); + expect(getByText("Custom content 1")).toBeTruthy(); + buttons[0] && fireEvent.click(buttons[0]); + expect(queryByText("Custom content 1")).not.toBeTruthy(); + }); + + test("Sorting by column works as expected", async () => { + const { getAllByRole } = render( + + ); + const headers = getAllByRole("columnheader"); + const sortableHeader = headers[1]; + + sortableHeader && fireEvent.click(sortableHeader); + expect(sortableHeader?.getAttribute("aria-sort")).toBe("ascending"); + await waitFor(() => { + const cells = getAllByRole("gridcell"); + expect(cells[1]?.textContent).toBe("1"); + }); + sortableHeader && fireEvent.click(sortableHeader); + expect(sortableHeader?.getAttribute("aria-sort")).toBe("descending"); + await waitFor(() => { + const cells = getAllByRole("gridcell"); + expect(cells[1]?.textContent).toBe("5"); + }); + }); + + test("Expands multiple rows at once", () => { + const { getAllByRole, getByText } = render( + + ); + + const buttons = getAllByRole("button"); + buttons[0] && fireEvent.click(buttons[0]); + buttons[1] && fireEvent.click(buttons[1]); + + expect(getByText("Custom content 1")).toBeTruthy(); + expect(getByText("Custom content 2")).toBeTruthy(); + }); }); diff --git a/packages/lib/src/nav-tabs/NavTabs.test.tsx b/packages/lib/src/nav-tabs/NavTabs.test.tsx index 849c7ceea7..a18a9868cc 100644 --- a/packages/lib/src/nav-tabs/NavTabs.test.tsx +++ b/packages/lib/src/nav-tabs/NavTabs.test.tsx @@ -69,4 +69,47 @@ describe("Tabs component tests", () => { expect(tabs[1]?.getAttribute("tabindex")).toBe("-1"); expect(tabs[2]?.getAttribute("tabindex")).toBe("3"); }); + + // test("Keyboard navigation changes focus on arrow keys", () => { + // const { getByRole, getAllByRole } = render( + // + // Tab 1 + // Tab 2 + // Tab 3 + // + // ); + + // const tablist = getByRole("tablist"); + // const tabs = getAllByRole("tab"); + + // expect(tabs[2]?.getAttribute("aria-selected")).toBe("true"); + + // fireEvent.keyDown(tablist, { key: "ArrowLeft" }); + // expect(tabs[0]?.getAttribute("tabindex")).toBe("0"); + + // fireEvent.keyDown(tablist, { key: "ArrowRight" }); + // expect(tabs[2]?.getAttribute("tabindex")).toBe("0"); + // }); + + test("Disabled tabs have aria-disabled and cannot be tab-focused", () => { + const { getAllByRole } = render( + + Disabled Tab + Active Tab + + ); + + const tabs = getAllByRole("tab"); + expect(tabs[0]?.getAttribute("aria-disabled")).toBe("true"); + expect(tabs[0]?.getAttribute("tabindex")).toBe("-1"); + }); + + test("Context passes correct iconPosition to children", () => { + const { getByText } = render( + + Tab 1 + + ); + expect(getByText("Tab 1")).toBeTruthy(); + }); }); diff --git a/packages/lib/src/nav-tabs/NavTabs.tsx b/packages/lib/src/nav-tabs/NavTabs.tsx index 880508f94e..923890b92f 100644 --- a/packages/lib/src/nav-tabs/NavTabs.tsx +++ b/packages/lib/src/nav-tabs/NavTabs.tsx @@ -22,7 +22,6 @@ const Underline = styled.div` const DxcNavTabs = ({ iconPosition = "left", tabIndex = 0, children }: NavTabsPropsType): JSX.Element => { const [innerFocusIndex, setInnerFocusIndex] = useState(null); - const childArray = Children.toArray(children).filter( (child) => typeof child === "object" && "props" in child ) as ReactElement[]; diff --git a/packages/lib/src/status-light/StatusLight.test.tsx b/packages/lib/src/status-light/StatusLight.test.tsx index a8b8f9d1a0..4403f4e29d 100644 --- a/packages/lib/src/status-light/StatusLight.test.tsx +++ b/packages/lib/src/status-light/StatusLight.test.tsx @@ -10,4 +10,10 @@ describe("StatusLight component tests", () => { const { getByRole } = render(); expect(getByRole("status")).toBeTruthy(); }); + + test("StatusLight dot is aria-hidden", () => { + const { container } = render(); + const dot = container.querySelector("div[aria-hidden='true']"); + expect(dot).toBeTruthy(); + }); }); diff --git a/packages/lib/src/toggle-group/ToggleGroup.test.tsx b/packages/lib/src/toggle-group/ToggleGroup.test.tsx index 1c95547373..941434b249 100644 --- a/packages/lib/src/toggle-group/ToggleGroup.test.tsx +++ b/packages/lib/src/toggle-group/ToggleGroup.test.tsx @@ -2,22 +2,15 @@ import { fireEvent, render } from "@testing-library/react"; import DxcToggleGroup from "./ToggleGroup"; const options = [ - { - value: 1, - label: "Amazon", - }, - { - value: 2, - label: "Ebay", - }, - { - value: 3, - label: "Apple", - }, - { - value: 4, - label: "Google", - }, + { value: 1, label: "Amazon" }, + { value: 2, label: "Ebay" }, + { value: 3, label: "Apple" }, + { value: 4, label: "Google" }, +]; + +const optionsWithDisabled = [ + { value: 1, label: "Amazon" }, + { value: 2, label: "Ebay", disabled: true }, ]; describe("Toggle group component tests", () => { @@ -93,4 +86,68 @@ describe("Toggle group component tests", () => { const toggleGroup = getByRole("toolbar"); expect(toggleGroup.getAttribute("aria-orientation")).toBe("vertical"); }); + test("Keyboard 'Enter' triggers onChange", () => { + const onChange = jest.fn(); + const { getByText } = render(); + const option = getByText("Amazon"); + option.focus(); + fireEvent.keyDown(option, { key: "Enter" }); + expect(onChange).toHaveBeenCalledTimes(1); + }); + test("Keyboard 'Space' triggers onChange", () => { + const onChange = jest.fn(); + const { getByText } = render(); + const option = getByText("Amazon"); + option.focus(); + fireEvent.keyDown(option, { key: " " }); + expect(onChange).toHaveBeenCalledTimes(1); + }); + test("Clicking a disabled button does not call onChange", () => { + const onChange = jest.fn(); + const { getByText } = render(); + const disabledOption = getByText("Ebay"); + fireEvent.click(disabledOption); + expect(onChange).not.toHaveBeenCalled(); + }); + test("Button only renders icon if label is missing", () => { + const icon = ( + + + + + ); + const iconOnlyOption = [{ value: 1, icon: icon, title: "Icon only" }]; + const { container, queryByText } = render(); + expect(container.querySelector("svg")).toBeTruthy(); + expect(queryByText("Icon only")).toBeFalsy(); + }); + test("Disabled buttons have tabIndex -1", () => { + const { getAllByRole } = render(); + const buttons = getAllByRole("button"); + expect(buttons[0]?.getAttribute("tabindex")).toBe("0"); + expect(buttons[1]?.getAttribute("tabindex")).toBe("-1"); + }); + test("Removes selected value when multiple is true and value is controlled", () => { + const handleChange = jest.fn(); + const options = [ + { value: 1, label: "Amazon" }, + { value: 2, label: "Ebay" }, + ]; + const { getByRole } = render(); + + fireEvent.click(getByRole("button", { name: "Ebay" })); + expect(handleChange).toHaveBeenCalledWith([1]); + }); + + test("Adds value when multiple is true and value is controlled", () => { + const handleChange = jest.fn(); + const options = [ + { value: 1, label: "Amazon" }, + { value: 2, label: "Ebay" }, + ]; + const { getByRole } = render(); + + fireEvent.click(getByRole("button", { name: "Ebay" })); + expect(handleChange).toHaveBeenCalledWith([1, 2]); + }); });