From 1df1dd345ab7d0cdc2e0b4eaa488845ec0140c37 Mon Sep 17 00:00:00 2001 From: IvanLazarov-TVM Date: Fri, 31 Jan 2025 16:13:13 +0200 Subject: [PATCH 1/4] Changed the TransferList to have combined search and check functionality and added one more test --- src/TransferList/TransferList.test.tsx | 81 ++++++++++++++-- src/TransferList/TransferList.tsx | 123 +++++++++++++------------ 2 files changed, 136 insertions(+), 68 deletions(-) diff --git a/src/TransferList/TransferList.test.tsx b/src/TransferList/TransferList.test.tsx index 1856a639..a94130a2 100644 --- a/src/TransferList/TransferList.test.tsx +++ b/src/TransferList/TransferList.test.tsx @@ -49,13 +49,15 @@ describe("TransferList", () => { /> ); - // test the source list heading and subheading + // test the source list heading expect(screen.getByText("Source List Label")); - expect(screen.getByText("0/3 selected")); - // test the target list heading and subheading + // test the target list heading expect(screen.getByText("Target List Label")); - expect(screen.getByText("0/0 selected")); + + // test the subheadings under the two headings + const zeroSelectedHeadings = screen.getAllByText("0 selected"); + expect(zeroSelectedHeadings.length).toBe(2); }); test("transfer buttons are disabled by default", () => { @@ -181,8 +183,9 @@ describe("TransferList", () => { expect(sourceItems[0].textContent).toBe("PearsConference"); expect(sourceItems[1].textContent).toBe("Oranges"); - // test selected items heading remains the same - expect(screen.getByText("0/2 selected")); + // test selected items heading remains the same and both headings for source and target are present + const zeroSelectedHeadings = screen.getAllByText("0 selected"); + expect(zeroSelectedHeadings.length).toBe(2); // get target list const targetList = screen.getByLabelText("Target List Label"); @@ -191,7 +194,6 @@ describe("TransferList", () => { // test existence of target list items expect(targetItems[0].textContent).toBe("Apples"); - expect(screen.getByText("0/1 selected")); }); test("clicked items toggle correctly, enable the relevant transfer button and update the heading", async () => { @@ -235,7 +237,7 @@ describe("TransferList", () => { ); // test subheading is correct - expect(screen.getByText("2/3 selected")); + expect(screen.getByText("2 selected")); // click first two items in list for a second time await user.click(items[0]); @@ -255,8 +257,9 @@ describe("TransferList", () => { false ); - // test subheading is correct - expect(screen.getByText("0/3 selected")); + // test selected items heading are both the same for source and target with all items unselected + const zeroSelectedHeadings = screen.getAllByText("0 selected"); + expect(zeroSelectedHeadings.length).toBe(2); }); test("if uncontrolled, checked items transfer from source to target list and uncheck", async () => { @@ -746,4 +749,62 @@ describe("TransferList", () => { // check the callback data expect(transferFn).toHaveBeenCalledWith(["Apples", "Pears", "Oranges"]); }); + + test("search for an item, check it, clear search, and verify selection", async () => { + // render component + const user = userEvent.setup(); + + render( + + ); + + // get search input + const searchInput = screen.getByLabelText("Search"); + + // search for "Pears" + await user.type(searchInput, "Pears"); + + // get filtered list + const sourceList = screen.getByLabelText("Source List Label"); + + // find the list item that contains "Pears" + const pearsItem = within(sourceList) + .getAllByRole("listitem") + .find(item => within(item).queryByText("Pears")); + + if (pearsItem) { + // get the checkbox + const pearsCheckbox = within(pearsItem).getByRole("checkbox"); + + // check "Pears" + await user.click(pearsCheckbox); + expect(pearsCheckbox).toHaveProperty("checked", true); + } + + // clear search + const clearButton = screen.getByLabelText("clear search"); + await user.click(clearButton); + + // get all items in source list after clearing search + const allItems = within(sourceList).getAllByRole("listitem"); + + // verify only "Pears" remains checked + allItems.forEach(item => { + // get the checkboxes + const checkbox = within(item).getByRole("checkbox"); + + // check for the item text content + if (item?.textContent?.includes("Pears")) { + // if the text is "Pears" the checkbox should be checked + expect(checkbox).toHaveProperty("checked", true); + } else { + // if the text is not "Pears" the checkbox should be false + expect(checkbox).toHaveProperty("checked", false); + } + }); + }); }); diff --git a/src/TransferList/TransferList.tsx b/src/TransferList/TransferList.tsx index 1c3e052c..e4df5ad4 100644 --- a/src/TransferList/TransferList.tsx +++ b/src/TransferList/TransferList.tsx @@ -8,13 +8,14 @@ import { Typography } from "@mui/material"; import React, { useLayoutEffect, useState } from "react"; -import SearchBar, { SearchBarProps } from "../SearchBar"; import { SingleListProps, TransferListItem, TransferListProps } from "./TransferList.types"; +import SearchBar from "../SearchBar"; + export default function TransferList({ defaultSelectedItems, items = [], @@ -96,10 +97,6 @@ export default function TransferList({ // All checked source list items const sourceItemsToTransfer = checked.filter(item => !keys.includes(item)); - // Boolean to indicate if all items are checked - const allSourceItemsChecked = - allSourceItems.length === sourceItemsToTransfer.length; - /** * Target list logic */ @@ -115,44 +112,52 @@ export default function TransferList({ // All checked target list items const targetItemsToTransfer = checked.filter(item => keys.includes(item)); - // Boolean to indicate if all target items are checked - const allTargetItemsChecked = - allTargetItems.length === targetItemsToTransfer.length; - /** * Handle check all items in the source list */ const handleCheckAllSource = () => { - // If all source items are checked, uncheck them - // Otherwise, check all source items - allSourceItemsChecked - ? setChecked( - checked.filter(item => !sourceItemsToTransfer.includes(item)) - ) - : setChecked([ - ...checked.filter(item => !sourceItemsToTransfer.includes(item)), - ...items - .map(item => filterKey(item)) - .filter(item => !keys.includes(item)) - ]); + // Get the items depending on if there is a search + const sourceItemsToCheck = sourceFilter + ? filteredSourceItems + : allSourceItems; + + // Get the items keys + const sourceItemKeys = sourceItemsToCheck.map(item => filterKey(item)); + + if (sourceItemKeys.every(item => checked.includes(item))) { + // If all filtered items are checked, uncheck only those + setChecked(checked.filter(item => !sourceItemKeys.includes(item))); + } else { + // Otherwise, check all filtered items + setChecked([ + ...checked, + ...sourceItemKeys.filter(item => !checked.includes(item)) + ]); + } }; /** * Handle check all items in the target list */ const handleCheckAllTarget = () => { - // If all target items are checked, uncheck them - // Otherwise, check all target items - allTargetItemsChecked - ? setChecked( - checked.filter(item => !targetItemsToTransfer.includes(item)) - ) - : setChecked([ - ...checked.filter(item => !targetItemsToTransfer.includes(item)), - ...items - .map(item => filterKey(item)) - .filter(item => keys.includes(item)) - ]); + // Get the items depending on if there is a search + const targetItemsToCheck = targetFilter + ? filteredTargetItems + : allTargetItems; + + // Get the items keys + const targetItemKeys = targetItemsToCheck.map(item => filterKey(item)); + + if (targetItemKeys.every(item => checked.includes(item))) { + // If all filtered items are checked, uncheck only those + setChecked(checked.filter(item => !targetItemKeys.includes(item))); + } else { + // Otherwise, check all filtered items + setChecked([ + ...checked, + ...targetItemKeys.filter(item => !checked.includes(item)) + ]); + } }; /** @@ -186,6 +191,12 @@ export default function TransferList({ // Get checked source items const checkedSourceItems = checked.filter(item => !keys.includes(item)); + // Remove the source search string on transfer + setSourceFilter(""); + + // Remove the target search string on transfer + setTargetFilter(""); + // Updated target list keys const updatedTargetList = [...keys, ...checkedSourceItems]; @@ -216,6 +227,12 @@ export default function TransferList({ item => !targetItemsToTransfer.includes(item) ); + // Remove the source search string on transfer + setSourceFilter(""); + + // Remove the target search string on transfer + setTargetFilter(""); + // Get the items that have been transferred const updatedSelectedItems = getTransferredItems(items, newTargetSelection); @@ -307,13 +324,13 @@ export default function TransferList({ {`${sourceItemsToTransfer.length}/${allSourceItems.length} selected`} + >{`${sourceItemsToTransfer.length} selected`} {allSourceItems.length > 0 ? ( - + ) : null} @@ -415,13 +432,13 @@ export default function TransferList({ {`${targetItemsToTransfer.length}/${allTargetItems.length} selected`} + >{`${targetItemsToTransfer.length} selected`} {allTargetItems.length > 0 ? ( - + ) : null} @@ -454,27 +471,17 @@ export default function TransferList({ /** * Search bar for the transfer list */ -const Search = ({ onChange }: { onChange: (value: string) => void }) => { - const [search, setSearch] = useState(""); - - // handle change - const handleChange: SearchBarProps["onChange"] = event => { - setSearch(event.target.value); - onChange(event.target.value); - }; - - return ( - - - - ); -}; +const Search = ({ + value, + onChange +}: { + value: string; + onChange: (value: string) => void; +}) => ( + + onChange(e.target.value)} /> + +); /** * Single list component for the transfer list From f146c06650e7b461fc2edf61b36c44b871da388f Mon Sep 17 00:00:00 2001 From: lukemojo <143453762+lukemojo@users.noreply.github.com> Date: Fri, 31 Jan 2025 15:09:30 +0000 Subject: [PATCH 2/4] 11.1.0-0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d16fad90..5885c204 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ipguk/react-ui", - "version": "10.4.0", + "version": "11.1.0-0", "description": "React UI component library for IPG web applications", "author": { "name": "IPG-Automotive-UK" From 2cb0a91d4ee6d4fb90d5b99d6a79912ca0221c7e Mon Sep 17 00:00:00 2001 From: IvanLazarov-TVM Date: Wed, 5 Feb 2025 10:28:16 +0200 Subject: [PATCH 3/4] Addressed the PR feedback, added a few tests --- src/TransferList/TransferList.test.tsx | 1771 +++++++++++++----------- src/TransferList/TransferList.tsx | 69 +- src/TransferList/TransferList.types.ts | 164 ++- 3 files changed, 1084 insertions(+), 920 deletions(-) diff --git a/src/TransferList/TransferList.test.tsx b/src/TransferList/TransferList.test.tsx index a94130a2..57b738ba 100644 --- a/src/TransferList/TransferList.test.tsx +++ b/src/TransferList/TransferList.test.tsx @@ -1,810 +1,961 @@ -import { describe, expect, test, vi } from "vitest"; -import { render, screen, within } from "@testing-library/react"; - -import React from "react"; -import TransferList from "."; -import userEvent from "@testing-library/user-event"; - -// item array using an object array structure -const defaultItemArray = [ - { key: "Apples", primaryLabel: "Apples" }, - { key: "Pears", primaryLabel: "Pears", secondaryLabel: "Conference" }, - { key: "Oranges", primaryLabel: "Oranges" } -]; - -// item array using strings -const stringItemArray = ["Apples", "Pears", "Oranges"]; - -describe("TransferList", () => { - test("list of unfiltered items in the source list", () => { - // render component - render( - - ); - - // get list - const list = screen.getByLabelText("Source List Label"); - - // get all list entries - const { getAllByRole } = within(list); - const items = getAllByRole("listitem"); - - // test existence of correct list items - expect(items[0].textContent).toBe("Apples"); - expect(items[1].textContent).toBe("PearsConference"); - expect(items[2].textContent).toBe("Oranges"); - }); - - test("list heading text", () => { - // render component - render( - - ); - - // test the source list heading - expect(screen.getByText("Source List Label")); - - // test the target list heading - expect(screen.getByText("Target List Label")); - - // test the subheadings under the two headings - const zeroSelectedHeadings = screen.getAllByText("0 selected"); - expect(zeroSelectedHeadings.length).toBe(2); - }); - - test("transfer buttons are disabled by default", () => { - // render component - render( - - ); - - // test transfer buttons exist and are disabled by default - expect(screen.getByLabelText("transfer to target list")).toHaveProperty( - "disabled", - true - ); - expect(screen.getByLabelText("transfer to source list")).toHaveProperty( - "disabled", - true - ); - }); - - test("source search bar filters the source list", async () => { - // render component - const user = userEvent.setup(); - render( - - ); - - // get search bar - const input = screen.getAllByLabelText("Search"); - - // get list entries - const list = screen.getByLabelText("Source List Label"); - const { getAllByRole } = within(list); - const items = getAllByRole("listitem"); - - // get initially rendered list - expect(items[0].textContent).toBe("Apples"); - expect(items[1].textContent).toBe("PearsConference"); - expect(items[2].textContent).toBe("Oranges"); - - // Filter the list - await user.type(input[0], "p"); - - // check if the source list has been filtered - const filteredItems = getAllByRole("listitem"); - expect(filteredItems[2]).toBeUndefined(); - }); - - test("search bar only appears when the list has entries", () => { - render( - - ); - - // get search bar - const inputs = screen.getAllByLabelText("Search"); - - // Check that only one search bar is rendered - expect(inputs.length).toBe(1); - }); - - test("clearing the source filter brings back the full source list", async () => { - // render component - const user = userEvent.setup(); - - render( - - ); - - // get search bar - const input = screen.getAllByLabelText("Search"); - - // get list entries - const list = screen.getByLabelText("Source List Label"); - const { getAllByRole } = within(list); - - // filter the list - await user.type(input[0], "p"); - - // clear the filter - const clearButton = screen.getByLabelText("clear search"); - await user.click(clearButton); - - // test if the source list has been unfiltered - const items = getAllByRole("listitem"); - expect(items[0].textContent).toBe("Apples"); - expect(items[1].textContent).toBe("PearsConference"); - expect(items[2].textContent).toBe("Oranges"); - }); - - test("renders items in the target list and not the source list if selected items are provided", () => { - // render component - render( - - ); - - // get source list - const sourceList = screen.getByLabelText("Source List Label"); - // get all list entries - const sourceItems = within(sourceList).getAllByRole("listitem"); - - // test existence of source list items - expect(sourceItems[0].textContent).toBe("PearsConference"); - expect(sourceItems[1].textContent).toBe("Oranges"); - - // test selected items heading remains the same and both headings for source and target are present - const zeroSelectedHeadings = screen.getAllByText("0 selected"); - expect(zeroSelectedHeadings.length).toBe(2); - - // get target list - const targetList = screen.getByLabelText("Target List Label"); - // get all list entries - const targetItems = within(targetList).getAllByRole("listitem"); - - // test existence of target list items - expect(targetItems[0].textContent).toBe("Apples"); - }); - - test("clicked items toggle correctly, enable the relevant transfer button and update the heading", async () => { - // render component - const user = userEvent.setup(); - render( - - ); - - // get list entries - const list = screen.getByLabelText("Source List Label"); - const { getAllByRole } = within(list); - const items = getAllByRole("listitem"); - - // click first two items in list - await user.click(items[0]); - await user.click(items[1]); - - // test button is enabled - expect(screen.getByLabelText("transfer to target list")).toHaveProperty( - "disabled", - false - ); - - // test checked status of checkboxes - expect(within(items[0]).getByRole("checkbox")).toHaveProperty( - "checked", - true - ); - expect(within(items[1]).getByRole("checkbox")).toHaveProperty( - "checked", - true - ); - expect(within(items[2]).getByRole("checkbox")).toHaveProperty( - "checked", - false - ); - - // test subheading is correct - expect(screen.getByText("2 selected")); - - // click first two items in list for a second time - await user.click(items[0]); - await user.click(items[1]); - - // test checked status of checkboxes - expect(within(items[0]).getByRole("checkbox")).toHaveProperty( - "checked", - false - ); - expect(within(items[1]).getByRole("checkbox")).toHaveProperty( - "checked", - false - ); - expect(within(items[2]).getByRole("checkbox")).toHaveProperty( - "checked", - false - ); - - // test selected items heading are both the same for source and target with all items unselected - const zeroSelectedHeadings = screen.getAllByText("0 selected"); - expect(zeroSelectedHeadings.length).toBe(2); - }); - - test("if uncontrolled, checked items transfer from source to target list and uncheck", async () => { - // render component - const user = userEvent.setup(); - - render( - - ); - - // get list entries - const sourceList = screen.getByLabelText("Source List Label"); - const sourceItems = within(sourceList).getAllByRole("listitem"); - - // get transfer to target button - const transferToTargetButton = screen.getByLabelText( - "transfer to target list" - ); - - // test the list entries - expect(sourceItems.length).toBe(3); - expect(sourceItems[0].textContent).toBe("Apples"); - expect(sourceItems[1].textContent).toBe("PearsConference"); - expect(sourceItems[2].textContent).toBe("Oranges"); - - // click first two items in list - await user.click(sourceItems[0]); - await user.click(sourceItems[1]); - - // click transfer button - await user.click(transferToTargetButton); - - // get target entries - const targetList = screen.getByLabelText("Target List Label"); - const targetItems = within(targetList).getAllByRole("listitem"); - - // test the target entries - expect(targetItems.length).toBe(2); - expect(targetItems[0].textContent).toBe("Apples"); - expect(targetItems[1].textContent).toBe("PearsConference"); - - // check updated source entries - const updatedSourceList = screen.getByLabelText("Source List Label"); - const updatedSourceItems = - within(updatedSourceList).getAllByRole("listitem"); - - // test the updated entries - expect(updatedSourceItems.length).toBe(1); - expect(updatedSourceItems[0].textContent).toBe("Oranges"); - - // test that everything is unchecked - expect(within(updatedSourceItems[0]).getByRole("checkbox")).toHaveProperty( - "checked", - false - ); - expect(within(targetItems[0]).getByRole("checkbox")).toHaveProperty( - "checked", - false - ); - expect(within(targetItems[1]).getByRole("checkbox")).toHaveProperty( - "checked", - false - ); - }); - - test("if controlled, checked items do not transfer unless selected items is updated", async () => { - // render component - const user = userEvent.setup(); - - // transfer function - const transferFn = vi.fn(); - - const { rerender } = render( - - ); - - // get list entries - const sourceList = screen.getByLabelText("Source List Label"); - const sourceItems = within(sourceList).getAllByRole("listitem"); - const targetList = screen.getByLabelText("Target List Label"); - const targetItems = within(targetList).queryAllByRole("listitem"); - - // get transfer to target button - const transferToTargetButton = screen.getByLabelText( - "transfer to target list" - ); - - // Test the list lengths - expect(sourceItems.length).toBe(3); - expect(targetItems.length).toBe(0); - - // click first two items in list - await user.click(sourceItems[0]); - await user.click(sourceItems[1]); - - // click transfer button - await user.click(transferToTargetButton); - - // get unchanged list entries - const unchangedSourceItems = within(sourceList).getAllByRole("listitem"); - const unchangedTargetItems = within(targetList).queryAllByRole("listitem"); - - // test onChange was called with the correct items - expect(transferFn).toHaveBeenCalledWith([ - { key: "Apples", primaryLabel: "Apples" }, - { key: "Pears", primaryLabel: "Pears", secondaryLabel: "Conference" } - ]); - - // test the list lengths remain the same - expect(unchangedSourceItems.length).toBe(3); - expect(unchangedTargetItems.length).toBe(0); - - rerender( - - ); - - // get updated target entries - const updatedSourceItems = within(sourceList).queryAllByRole("listitem"); - const updatedTargetItems = within(targetList).getAllByRole("listitem"); - - // test that selected items has updated the lists - expect(updatedSourceItems.length).toBe(1); - expect(updatedTargetItems.length).toBe(2); - - // test that everything is unchecked - expect(within(updatedSourceItems[0]).getByRole("checkbox")).toHaveProperty( - "checked", - false - ); - expect(within(updatedTargetItems[0]).getByRole("checkbox")).toHaveProperty( - "checked", - false - ); - expect(within(updatedTargetItems[1]).getByRole("checkbox")).toHaveProperty( - "checked", - false - ); - }); - - test("select all checkboxes are disabled unless the relevant list has entries", () => { - // render component - render( - - ); - - // get both checkboxes in the headers - const selectAllSourceCheckbox = within( - screen.getByLabelText("select all source list items") - ).getByRole("checkbox"); - - const selectAllTargetCheckbox = within( - screen.getByLabelText("select all target list items") - ).getByRole("checkbox"); - - // test transfer buttons exist and are disabled by default - expect(selectAllSourceCheckbox).toHaveProperty("disabled", true); - expect(selectAllTargetCheckbox).toHaveProperty("disabled", true); - }); - - test("source list select all checkbox selects all the source list items", async () => { - // render component - const user = userEvent.setup(); - - render( - - ); - - // get checkboxes - const selectAllSourceCheckbox = within( - screen.getByLabelText("select all source list items") - ).getByRole("checkbox"); - - // test transfer buttons exist and are disabled and enabled correctly - expect(selectAllSourceCheckbox).toHaveProperty("disabled", false); - - // click select all source checkbox - await user.click(selectAllSourceCheckbox); - - // check updated source entries - const sourceList = screen.getByLabelText("Source List Label"); - const sourceItems = within(sourceList).getAllByRole("listitem"); - - // all source list elements should be checked - expect(within(sourceItems[0]).getByRole("checkbox")).toHaveProperty( - "checked", - true - ); - expect(within(sourceItems[1]).getByRole("checkbox")).toHaveProperty( - "checked", - true - ); - expect(within(sourceItems[2]).getByRole("checkbox")).toHaveProperty( - "checked", - true - ); - }); - - test("source list select all checkbox clears the source list items if all are selected", async () => { - // render component - const user = userEvent.setup(); - - render( - - ); - - // get checkboxes - const selectAllSourceCheckbox = within( - screen.getByLabelText("select all source list items") - ).getByRole("checkbox"); - - // test transfer buttons exist and are disabled and enabled correctly - expect(selectAllSourceCheckbox).toHaveProperty("disabled", false); - - // click select all source checkbox - await user.click(selectAllSourceCheckbox); - - // click select all source checkbox a second time - await user.click(selectAllSourceCheckbox); - - // check updated source entries - const sourceList = screen.getByLabelText("Source List Label"); - const sourceItems = within(sourceList).getAllByRole("listitem"); - - // all source list elements should be checked - expect(within(sourceItems[0]).getByRole("checkbox")).toHaveProperty( - "checked", - false - ); - expect(within(sourceItems[1]).getByRole("checkbox")).toHaveProperty( - "checked", - false - ); - expect(within(sourceItems[2]).getByRole("checkbox")).toHaveProperty( - "checked", - false - ); - }); - - test("target list select all checkbox checks all the selected list items", async () => { - // render component - const user = userEvent.setup(); - - render( - - ); - - // get checkboxes - const selectAllTargetCheckbox = within( - screen.getByLabelText("select all target list items") - ).getByRole("checkbox"); - - // test transfer buttons exist and are disabled and enabled correctly - expect(selectAllTargetCheckbox).toHaveProperty("disabled", false); - - // click select all target checkbox - await user.click(selectAllTargetCheckbox); - - // check updated source entries - const targetList = screen.getByLabelText("Target List Label"); - const targetItems = within(targetList).getAllByRole("listitem"); - - // all source list elements should be checked - expect(within(targetItems[0]).getByRole("checkbox")).toHaveProperty( - "checked", - true - ); - - expect(within(targetItems[1]).getByRole("checkbox")).toHaveProperty( - "checked", - true - ); - - expect(within(targetItems[2]).getByRole("checkbox")).toHaveProperty( - "checked", - true - ); - }); - - test("target list select all checkbox clears the selected list items if all are selected", async () => { - // render component - const user = userEvent.setup(); - - render( - - ); - - // get checkboxes - const selectAllTargetCheckbox = within( - screen.getByLabelText("select all target list items") - ).getByRole("checkbox"); - - // click select all target checkbox - await user.click(selectAllTargetCheckbox); - - // click select all target checkbox a second time - await user.click(selectAllTargetCheckbox); - - // check updated source entries - const targetList = screen.getByLabelText("Target List Label"); - const targetItems = within(targetList).getAllByRole("listitem"); - - // all source list elements should be checked - expect(within(targetItems[0]).getByRole("checkbox")).toHaveProperty( - "checked", - false - ); - - expect(within(targetItems[1]).getByRole("checkbox")).toHaveProperty( - "checked", - false - ); - - expect(within(targetItems[2]).getByRole("checkbox")).toHaveProperty( - "checked", - false - ); - }); - - test("transfer to target fires onChange callback with selected items", async () => { - // render component - const user = userEvent.setup(); - // transfer function - const transferFn = vi.fn(); - - render( - - ); - - // get checkboxes - const selectAllSourceCheckbox = within( - screen.getByLabelText("select all source list items") - ).getByRole("checkbox"); - - const selectAllTargetCheckbox = within( - screen.getByLabelText("select all target list items") - ).getByRole("checkbox"); - - // test transfer buttons exist and are disabled and enabled correctly - expect(selectAllSourceCheckbox).toHaveProperty("disabled", true); - expect(selectAllTargetCheckbox).toHaveProperty("disabled", false); - - // select all the target list items - await user.click(selectAllTargetCheckbox); - - // get transfer to source button - const transferToSourceButton = screen.getByLabelText( - "transfer to source list" - ); - - // transfer all target list items to source - await user.click(transferToSourceButton); - - // check the callback data - expect(transferFn).toHaveBeenCalledWith([]); - - // select all the source list items - await user.click(selectAllSourceCheckbox); - - // get transfer to target button - const transferToTargetButton = screen.getByLabelText( - "transfer to target list" - ); - - // transfer all source list items to target - await user.click(transferToTargetButton); - - // check the callback data - expect(transferFn).toHaveBeenCalledWith([ - { key: "Apples", primaryLabel: "Apples" }, - { key: "Pears", primaryLabel: "Pears", secondaryLabel: "Conference" }, - { key: "Oranges", primaryLabel: "Oranges" } - ]); - }); - - test("transfer to source fires onChange callback with selected items array", async () => { - // render component - const user = userEvent.setup(); - // transfer function - const transferFn = vi.fn(); - - render( - - ); - - // get select all target checkbox - const selectAllTargetCheckbox = within( - screen.getByLabelText("select all target list items") - ).getByRole("checkbox"); - - // select all the target list items - await user.click(selectAllTargetCheckbox); - - // get transfer to source button - const transferToSourceButton = screen.getByLabelText( - "transfer to source list" - ); - - // transfer all target list items to source - await user.click(transferToSourceButton); - - // check the callback data - expect(transferFn).toHaveBeenCalledWith([]); - }); - - test("accepts array of strings as items and returns them", async () => { - // render component - const user = userEvent.setup(); - - // transfer function - const transferFn = vi.fn(); - - render( - - ); - - // get all source checkbox - const selectAllSourceCheckbox = within( - screen.getByLabelText("select all source list items") - ).getByRole("checkbox"); - - // select all the target list items - await user.click(selectAllSourceCheckbox); - - // get transfer to source button - const transferToTargetButton = screen.getByLabelText( - "transfer to target list" - ); - - // transfer all target list items to source - await user.click(transferToTargetButton); - - // check the callback data - expect(transferFn).toHaveBeenCalledWith(["Apples", "Pears", "Oranges"]); - }); - - test("search for an item, check it, clear search, and verify selection", async () => { - // render component - const user = userEvent.setup(); - - render( - - ); - - // get search input - const searchInput = screen.getByLabelText("Search"); - - // search for "Pears" - await user.type(searchInput, "Pears"); - - // get filtered list - const sourceList = screen.getByLabelText("Source List Label"); - - // find the list item that contains "Pears" - const pearsItem = within(sourceList) - .getAllByRole("listitem") - .find(item => within(item).queryByText("Pears")); - - if (pearsItem) { - // get the checkbox - const pearsCheckbox = within(pearsItem).getByRole("checkbox"); - - // check "Pears" - await user.click(pearsCheckbox); - expect(pearsCheckbox).toHaveProperty("checked", true); - } - - // clear search - const clearButton = screen.getByLabelText("clear search"); - await user.click(clearButton); - - // get all items in source list after clearing search - const allItems = within(sourceList).getAllByRole("listitem"); - - // verify only "Pears" remains checked - allItems.forEach(item => { - // get the checkboxes - const checkbox = within(item).getByRole("checkbox"); - - // check for the item text content - if (item?.textContent?.includes("Pears")) { - // if the text is "Pears" the checkbox should be checked - expect(checkbox).toHaveProperty("checked", true); - } else { - // if the text is not "Pears" the checkbox should be false - expect(checkbox).toHaveProperty("checked", false); - } - }); - }); -}); +import { describe, expect, test, vi } from "vitest"; +import { render, screen, within } from "@testing-library/react"; + +import React from "react"; +import TransferList from "."; +import userEvent from "@testing-library/user-event"; + +// item array using an object array structure +const defaultItemArray = [ + { key: "Apples", primaryLabel: "Apples" }, + { key: "Pears", primaryLabel: "Pears", secondaryLabel: "Conference" }, + { key: "Oranges", primaryLabel: "Oranges" } +]; + +// item array using strings +const stringItemArray = ["Apples", "Pears", "Oranges"]; + +describe("TransferList", () => { + test("list of unfiltered items in the source list", () => { + // render component + render( + + ); + + // get list + const list = screen.getByLabelText("Source List Label"); + + // get all list entries + const { getAllByRole } = within(list); + const items = getAllByRole("listitem"); + + // test existence of correct list items + expect(items[0].textContent).toBe("Apples"); + expect(items[1].textContent).toBe("PearsConference"); + expect(items[2].textContent).toBe("Oranges"); + }); + + test("list heading text", () => { + // render component + render( + + ); + + // test the source list heading + expect(screen.getByText("Source List Label")); + + // test the target list heading + expect(screen.getByText("Target List Label")); + + // test the subheadings under the two headings + const zeroSelectedHeadings = screen.getAllByText("0 selected"); + expect(zeroSelectedHeadings.length).toBe(2); + }); + + test("transfer buttons are disabled by default", () => { + // render component + render( + + ); + + // test transfer buttons exist and are disabled by default + expect(screen.getByLabelText("transfer to target list")).toHaveProperty( + "disabled", + true + ); + expect(screen.getByLabelText("transfer to source list")).toHaveProperty( + "disabled", + true + ); + }); + + test("source search bar filters the source list", async () => { + // render component + const user = userEvent.setup(); + render( + + ); + + // get search bar + const input = screen.getAllByLabelText("Search"); + + // get list entries + const list = screen.getByLabelText("Source List Label"); + const { getAllByRole } = within(list); + const items = getAllByRole("listitem"); + + // get initially rendered list + expect(items[0].textContent).toBe("Apples"); + expect(items[1].textContent).toBe("PearsConference"); + expect(items[2].textContent).toBe("Oranges"); + + // Filter the list + await user.type(input[0], "p"); + + // check if the source list has been filtered + const filteredItems = getAllByRole("listitem"); + expect(filteredItems[2]).toBeUndefined(); + }); + + test("search bar only appears when the list has entries", () => { + render( + + ); + + // get search bar + const inputs = screen.getAllByLabelText("Search"); + + // Check that only one search bar is rendered + expect(inputs.length).toBe(1); + }); + + test("clearing the source filter brings back the full source list", async () => { + // render component + const user = userEvent.setup(); + + render( + + ); + + // get search bar + const input = screen.getAllByLabelText("Search"); + + // get list entries + const list = screen.getByLabelText("Source List Label"); + const { getAllByRole } = within(list); + + // filter the list + await user.type(input[0], "p"); + + // clear the filter + const clearButton = screen.getByLabelText("clear search"); + await user.click(clearButton); + + // test if the source list has been unfiltered + const items = getAllByRole("listitem"); + expect(items[0].textContent).toBe("Apples"); + expect(items[1].textContent).toBe("PearsConference"); + expect(items[2].textContent).toBe("Oranges"); + }); + + test("renders items in the target list and not the source list if selected items are provided", () => { + // render component + render( + + ); + + // get source list + const sourceList = screen.getByLabelText("Source List Label"); + // get all list entries + const sourceItems = within(sourceList).getAllByRole("listitem"); + + // test existence of source list items + expect(sourceItems[0].textContent).toBe("PearsConference"); + expect(sourceItems[1].textContent).toBe("Oranges"); + + // test selected items heading remains the same and both headings for source and target are present + const zeroSelectedHeadings = screen.getAllByText("0 selected"); + expect(zeroSelectedHeadings.length).toBe(2); + + // get target list + const targetList = screen.getByLabelText("Target List Label"); + // get all list entries + const targetItems = within(targetList).getAllByRole("listitem"); + + // test existence of target list items + expect(targetItems[0].textContent).toBe("Apples"); + }); + + test("clicked items toggle correctly, enable the relevant transfer button and update the heading", async () => { + // render component + const user = userEvent.setup(); + render( + + ); + + // get list entries + const list = screen.getByLabelText("Source List Label"); + const { getAllByRole } = within(list); + const items = getAllByRole("listitem"); + + // click first two items in list + await user.click(items[0]); + await user.click(items[1]); + + // test button is enabled + expect(screen.getByLabelText("transfer to target list")).toHaveProperty( + "disabled", + false + ); + + // test checked status of checkboxes + expect(within(items[0]).getByRole("checkbox")).toHaveProperty( + "checked", + true + ); + expect(within(items[1]).getByRole("checkbox")).toHaveProperty( + "checked", + true + ); + expect(within(items[2]).getByRole("checkbox")).toHaveProperty( + "checked", + false + ); + + // test subheading is correct + expect(screen.getByText("2 selected")); + + // make sure target shows 0 selected + const zeroSelectedHeading = screen.getAllByText("0 selected"); + expect(zeroSelectedHeading.length).toBe(1); + + // click first two items in list for a second time + await user.click(items[0]); + await user.click(items[1]); + + // test checked status of checkboxes + expect(within(items[0]).getByRole("checkbox")).toHaveProperty( + "checked", + false + ); + expect(within(items[1]).getByRole("checkbox")).toHaveProperty( + "checked", + false + ); + expect(within(items[2]).getByRole("checkbox")).toHaveProperty( + "checked", + false + ); + + // test selected items heading are both the same for source and target with all items unselected + const zeroSelectedHeadings = screen.getAllByText("0 selected"); + expect(zeroSelectedHeadings.length).toBe(2); + }); + + test("if uncontrolled, checked items transfer from source to target list and uncheck", async () => { + // render component + const user = userEvent.setup(); + + render( + + ); + + // get list entries + const sourceList = screen.getByLabelText("Source List Label"); + const sourceItems = within(sourceList).getAllByRole("listitem"); + + // get transfer to target button + const transferToTargetButton = screen.getByLabelText( + "transfer to target list" + ); + + // test the list entries + expect(sourceItems.length).toBe(3); + expect(sourceItems[0].textContent).toBe("Apples"); + expect(sourceItems[1].textContent).toBe("PearsConference"); + expect(sourceItems[2].textContent).toBe("Oranges"); + + // click first two items in list + await user.click(sourceItems[0]); + await user.click(sourceItems[1]); + + // click transfer button + await user.click(transferToTargetButton); + + // get target entries + const targetList = screen.getByLabelText("Target List Label"); + const targetItems = within(targetList).getAllByRole("listitem"); + + // test the target entries + expect(targetItems.length).toBe(2); + expect(targetItems[0].textContent).toBe("Apples"); + expect(targetItems[1].textContent).toBe("PearsConference"); + + // check updated source entries + const updatedSourceList = screen.getByLabelText("Source List Label"); + const updatedSourceItems = + within(updatedSourceList).getAllByRole("listitem"); + + // test the updated entries + expect(updatedSourceItems.length).toBe(1); + expect(updatedSourceItems[0].textContent).toBe("Oranges"); + + // test that everything is unchecked + expect(within(updatedSourceItems[0]).getByRole("checkbox")).toHaveProperty( + "checked", + false + ); + expect(within(targetItems[0]).getByRole("checkbox")).toHaveProperty( + "checked", + false + ); + expect(within(targetItems[1]).getByRole("checkbox")).toHaveProperty( + "checked", + false + ); + }); + + test("if controlled, checked items do not transfer unless selected items is updated", async () => { + // render component + const user = userEvent.setup(); + + // transfer function + const transferFn = vi.fn(); + + const { rerender } = render( + + ); + + // get list entries + const sourceList = screen.getByLabelText("Source List Label"); + const sourceItems = within(sourceList).getAllByRole("listitem"); + const targetList = screen.getByLabelText("Target List Label"); + const targetItems = within(targetList).queryAllByRole("listitem"); + + // get transfer to target button + const transferToTargetButton = screen.getByLabelText( + "transfer to target list" + ); + + // Test the list lengths + expect(sourceItems.length).toBe(3); + expect(targetItems.length).toBe(0); + + // click first two items in list + await user.click(sourceItems[0]); + await user.click(sourceItems[1]); + + // click transfer button + await user.click(transferToTargetButton); + + // get unchanged list entries + const unchangedSourceItems = within(sourceList).getAllByRole("listitem"); + const unchangedTargetItems = within(targetList).queryAllByRole("listitem"); + + // test onChange was called with the correct items + expect(transferFn).toHaveBeenCalledWith([ + { key: "Apples", primaryLabel: "Apples" }, + { key: "Pears", primaryLabel: "Pears", secondaryLabel: "Conference" } + ]); + + // test the list lengths remain the same + expect(unchangedSourceItems.length).toBe(3); + expect(unchangedTargetItems.length).toBe(0); + + rerender( + + ); + + // get updated target entries + const updatedSourceItems = within(sourceList).queryAllByRole("listitem"); + const updatedTargetItems = within(targetList).getAllByRole("listitem"); + + // test that selected items has updated the lists + expect(updatedSourceItems.length).toBe(1); + expect(updatedTargetItems.length).toBe(2); + + // test that everything is unchecked + expect(within(updatedSourceItems[0]).getByRole("checkbox")).toHaveProperty( + "checked", + false + ); + expect(within(updatedTargetItems[0]).getByRole("checkbox")).toHaveProperty( + "checked", + false + ); + expect(within(updatedTargetItems[1]).getByRole("checkbox")).toHaveProperty( + "checked", + false + ); + }); + + test("select all checkboxes are disabled unless the relevant list has entries", () => { + // render component + render( + + ); + + // get both checkboxes in the headers + const selectAllSourceCheckbox = within( + screen.getByLabelText("select all source list items") + ).getByRole("checkbox"); + + const selectAllTargetCheckbox = within( + screen.getByLabelText("select all target list items") + ).getByRole("checkbox"); + + // test transfer buttons exist and are disabled by default + expect(selectAllSourceCheckbox).toHaveProperty("disabled", true); + expect(selectAllTargetCheckbox).toHaveProperty("disabled", true); + }); + + test("source list select all checkbox selects all the source list items", async () => { + // render component + const user = userEvent.setup(); + + render( + + ); + + // get checkboxes + const selectAllSourceCheckbox = within( + screen.getByLabelText("select all source list items") + ).getByRole("checkbox"); + + // test transfer buttons exist and are disabled and enabled correctly + expect(selectAllSourceCheckbox).toHaveProperty("disabled", false); + + // click select all source checkbox + await user.click(selectAllSourceCheckbox); + + // check updated source entries + const sourceList = screen.getByLabelText("Source List Label"); + const sourceItems = within(sourceList).getAllByRole("listitem"); + + // all source list elements should be checked + expect(within(sourceItems[0]).getByRole("checkbox")).toHaveProperty( + "checked", + true + ); + expect(within(sourceItems[1]).getByRole("checkbox")).toHaveProperty( + "checked", + true + ); + expect(within(sourceItems[2]).getByRole("checkbox")).toHaveProperty( + "checked", + true + ); + }); + + test("source list select all checkbox clears the source list items if all are selected", async () => { + // render component + const user = userEvent.setup(); + + render( + + ); + + // get checkboxes + const selectAllSourceCheckbox = within( + screen.getByLabelText("select all source list items") + ).getByRole("checkbox"); + + // test transfer buttons exist and are disabled and enabled correctly + expect(selectAllSourceCheckbox).toHaveProperty("disabled", false); + + // click select all source checkbox + await user.click(selectAllSourceCheckbox); + + // click select all source checkbox a second time + await user.click(selectAllSourceCheckbox); + + // check updated source entries + const sourceList = screen.getByLabelText("Source List Label"); + const sourceItems = within(sourceList).getAllByRole("listitem"); + + // all source list elements should be checked + expect(within(sourceItems[0]).getByRole("checkbox")).toHaveProperty( + "checked", + false + ); + expect(within(sourceItems[1]).getByRole("checkbox")).toHaveProperty( + "checked", + false + ); + expect(within(sourceItems[2]).getByRole("checkbox")).toHaveProperty( + "checked", + false + ); + }); + + test("target list select all checkbox checks all the selected list items", async () => { + // render component + const user = userEvent.setup(); + + render( + + ); + + // get checkboxes + const selectAllTargetCheckbox = within( + screen.getByLabelText("select all target list items") + ).getByRole("checkbox"); + + // test transfer buttons exist and are disabled and enabled correctly + expect(selectAllTargetCheckbox).toHaveProperty("disabled", false); + + // click select all target checkbox + await user.click(selectAllTargetCheckbox); + + // check updated source entries + const targetList = screen.getByLabelText("Target List Label"); + const targetItems = within(targetList).getAllByRole("listitem"); + + // all source list elements should be checked + expect(within(targetItems[0]).getByRole("checkbox")).toHaveProperty( + "checked", + true + ); + + expect(within(targetItems[1]).getByRole("checkbox")).toHaveProperty( + "checked", + true + ); + + expect(within(targetItems[2]).getByRole("checkbox")).toHaveProperty( + "checked", + true + ); + }); + + test("target list select all checkbox clears the selected list items if all are selected", async () => { + // render component + const user = userEvent.setup(); + + render( + + ); + + // get checkboxes + const selectAllTargetCheckbox = within( + screen.getByLabelText("select all target list items") + ).getByRole("checkbox"); + + // click select all target checkbox + await user.click(selectAllTargetCheckbox); + + // click select all target checkbox a second time + await user.click(selectAllTargetCheckbox); + + // check updated source entries + const targetList = screen.getByLabelText("Target List Label"); + const targetItems = within(targetList).getAllByRole("listitem"); + + // all source list elements should be checked + expect(within(targetItems[0]).getByRole("checkbox")).toHaveProperty( + "checked", + false + ); + + expect(within(targetItems[1]).getByRole("checkbox")).toHaveProperty( + "checked", + false + ); + + expect(within(targetItems[2]).getByRole("checkbox")).toHaveProperty( + "checked", + false + ); + }); + + test("transfer to target fires onChange callback with selected items", async () => { + // render component + const user = userEvent.setup(); + // transfer function + const transferFn = vi.fn(); + + render( + + ); + + // get checkboxes + const selectAllSourceCheckbox = within( + screen.getByLabelText("select all source list items") + ).getByRole("checkbox"); + + const selectAllTargetCheckbox = within( + screen.getByLabelText("select all target list items") + ).getByRole("checkbox"); + + // test transfer buttons exist and are disabled and enabled correctly + expect(selectAllSourceCheckbox).toHaveProperty("disabled", true); + expect(selectAllTargetCheckbox).toHaveProperty("disabled", false); + + // select all the target list items + await user.click(selectAllTargetCheckbox); + + // get transfer to source button + const transferToSourceButton = screen.getByLabelText( + "transfer to source list" + ); + + // transfer all target list items to source + await user.click(transferToSourceButton); + + // check the callback data + expect(transferFn).toHaveBeenCalledWith([]); + + // select all the source list items + await user.click(selectAllSourceCheckbox); + + // get transfer to target button + const transferToTargetButton = screen.getByLabelText( + "transfer to target list" + ); + + // transfer all source list items to target + await user.click(transferToTargetButton); + + // check the callback data + expect(transferFn).toHaveBeenCalledWith([ + { key: "Apples", primaryLabel: "Apples" }, + { key: "Pears", primaryLabel: "Pears", secondaryLabel: "Conference" }, + { key: "Oranges", primaryLabel: "Oranges" } + ]); + }); + + test("transfer to source fires onChange callback with selected items array", async () => { + // render component + const user = userEvent.setup(); + // transfer function + const transferFn = vi.fn(); + + render( + + ); + + // get select all target checkbox + const selectAllTargetCheckbox = within( + screen.getByLabelText("select all target list items") + ).getByRole("checkbox"); + + // select all the target list items + await user.click(selectAllTargetCheckbox); + + // get transfer to source button + const transferToSourceButton = screen.getByLabelText( + "transfer to source list" + ); + + // transfer all target list items to source + await user.click(transferToSourceButton); + + // check the callback data + expect(transferFn).toHaveBeenCalledWith([]); + }); + + test("accepts array of strings as items and returns them", async () => { + // render component + const user = userEvent.setup(); + + // transfer function + const transferFn = vi.fn(); + + render( + + ); + + // get all source checkbox + const selectAllSourceCheckbox = within( + screen.getByLabelText("select all source list items") + ).getByRole("checkbox"); + + // select all the target list items + await user.click(selectAllSourceCheckbox); + + // get transfer to source button + const transferToTargetButton = screen.getByLabelText( + "transfer to target list" + ); + + // transfer all target list items to source + await user.click(transferToTargetButton); + + // check the callback data + expect(transferFn).toHaveBeenCalledWith(["Apples", "Pears", "Oranges"]); + }); + + test("search for an item, check it, clear search, and verify selection", async () => { + // render component + const user = userEvent.setup(); + + render( + + ); + + // get search input + const searchInput = screen.getByLabelText("Search"); + + // search for "Pears" + await user.type(searchInput, "Pears"); + + // get filtered list + const sourceList = screen.getByLabelText("Source List Label"); + + // find the list item that contains "Pears" + const pearsItem = within(sourceList) + .getAllByRole("listitem") + .find(item => within(item).queryByText("Pears")); + + if (pearsItem) { + // get the checkbox + const pearsCheckbox = within(pearsItem).getByRole("checkbox"); + + // check "Pears" + await user.click(pearsCheckbox); + expect(pearsCheckbox).toHaveProperty("checked", true); + } + + // clear search + const clearButton = screen.getByLabelText("clear search"); + await user.click(clearButton); + + // get all items in source list after clearing search + const allItems = within(sourceList).getAllByRole("listitem"); + + // verify only "Pears" remains checked + allItems.forEach(item => { + // get the checkboxes + const checkbox = within(item).getByRole("checkbox"); + + // check for the item text content + if (item?.textContent?.includes("Pears")) { + // if the text is "Pears" the checkbox should be checked + expect(checkbox).toHaveProperty("checked", true); + } else { + // if the text is not "Pears" the checkbox should be false + expect(checkbox).toHaveProperty("checked", false); + } + }); + }); + + test("user selects an item, searches another, selects it, transfers both", async () => { + // render component + const user = userEvent.setup(); + + render( + + ); + + const sourceList = screen.getByLabelText("Source List"); + const sourceItems = within(sourceList).getAllByRole("listitem"); + + // select first item + await user.click(sourceItems[0]); + + // get search input + const searchInput = screen.getByLabelText("Search"); + await user.type(searchInput, "Pears"); + + // select the searched item + const filteredItems = within(sourceList).getAllByRole("listitem"); + await user.click(filteredItems[0]); + + // ensure label shows correct selected count + const selected = screen.getByText("2 selected"); + expect(selected).not.toBeNull(); + + // transfer items + await user.click(screen.getByLabelText("transfer to target list")); + + // verify items moved to target list + const targetList = screen.getByLabelText("Target List"); + const targetItems = within(targetList).getAllByRole("listitem"); + expect(targetItems.length).toBe(2); + }); + + test("select all toggles correctly with search", async () => { + // render component + const user = userEvent.setup(); + + render( + + ); + + const sourceList = screen.getByLabelText("Source List"); + + // select 'Oranges' + await user.click(within(sourceList).getByText("Oranges")); + + // get search input + const searchInput = screen.getByLabelText("Search"); + await user.type(searchInput, "Apples"); + + // select all and check if we have 2 selected("Oranges" and "Apples") + const selectAllCheckbox = screen.getByLabelText( + "select all source list items" + ); + await user.click(selectAllCheckbox); + expect(screen.getByText("2 selected")).not.toBeNull(); + + // deselect all + await user.click(selectAllCheckbox); + + // check if now only one have been left as selected("Oranges") + expect(screen.getByText("1 selected")).not.toBeNull(); + + // select all again and clear search + await user.click(selectAllCheckbox); + await user.type(searchInput, "{selectall}{backspace}"); + + // assert that after the search is cleared we still have both "Apples" and "Oranges" selected + expect(screen.getByText("2 selected")).not.toBeNull(); + + // deselect all + await user.click(selectAllCheckbox); + + // make sure both source target shows 0 selected after the diselection + const zeroSelectedHeadings = screen.getAllByText("0 selected"); + expect(zeroSelectedHeadings.length).toBe(1); + }); + + test("select all only affects its own list", async () => { + const user = userEvent.setup(); + + render( + + ); + + // get source and target lists + const sourceList = screen.getByLabelText("Source List Label"); + const targetList = screen.getByLabelText("Target List Label"); + + // verify source list has items + const sourceItems = within(sourceList).getAllByRole("listitem"); + expect(sourceItems.length).toBe(3); + + // select all checkboxes for source + const selectAllSource = within( + screen.getByLabelText("select all source list items") + ).getByRole("checkbox") as HTMLInputElement; + + // select all checkboxes for target + const selectAllTarget = within( + screen.getByLabelText("select all target list items") + ).getByRole("checkbox") as HTMLInputElement; + + // ensure source select all is enabled and target select all is disabled + expect(selectAllSource.disabled).toBe(false); + expect(selectAllTarget.disabled).toBe(true); + + // select all in source list + await user.click(selectAllSource); + expect(screen.getByText("3 selected")).toBeTruthy(); + expect(selectAllSource).toHaveProperty("checked", true); + + // get transfer button and move items to target list + const transferButton = screen.getByLabelText("transfer to target list"); + await user.click(transferButton); + + // verify target list now has items + const targetItems = within(targetList).getAllByRole("listitem"); + expect(targetItems.length).toBe(3); + + // ensure target select all is now enabled and source is disabled + expect(selectAllSource.disabled).toBe(true); + expect(selectAllTarget.disabled).toBe(false); + + // verify source list select all is unchecked and unaffected by the target check + expect(selectAllSource).toHaveProperty("checked", false); + + // select all in target list + await user.click(selectAllTarget); + expect(screen.getByText("3 selected")).toBeTruthy(); + expect(selectAllTarget).toHaveProperty("checked", true); + }); +}); diff --git a/src/TransferList/TransferList.tsx b/src/TransferList/TransferList.tsx index e4df5ad4..28535dfa 100644 --- a/src/TransferList/TransferList.tsx +++ b/src/TransferList/TransferList.tsx @@ -7,12 +7,13 @@ import { ListItemText, Typography } from "@mui/material"; -import React, { useLayoutEffect, useState } from "react"; import { + HandleCheckProps, SingleListProps, TransferListItem, TransferListProps } from "./TransferList.types"; +import React, { useLayoutEffect, useState } from "react"; import SearchBar from "../SearchBar"; @@ -113,52 +114,46 @@ export default function TransferList({ const targetItemsToTransfer = checked.filter(item => keys.includes(item)); /** - * Handle check all items in the source list + * Handle checking all items in a list (source or target) */ - const handleCheckAllSource = () => { - // Get the items depending on if there is a search - const sourceItemsToCheck = sourceFilter - ? filteredSourceItems - : allSourceItems; - - // Get the items keys - const sourceItemKeys = sourceItemsToCheck.map(item => filterKey(item)); - - if (sourceItemKeys.every(item => checked.includes(item))) { + const handleCheckAll = ({ + isFiltered, + allItems, + filteredItems + }: HandleCheckProps) => { + // Determine which items to check + const itemsToCheck = isFiltered ? filteredItems : allItems; + + // Extract the keys of the items + const itemKeys = itemsToCheck.map(item => filterKey(item)); + + if (itemKeys.every(item => checked.includes(item))) { // If all filtered items are checked, uncheck only those - setChecked(checked.filter(item => !sourceItemKeys.includes(item))); + setChecked(checked.filter(item => !itemKeys.includes(item))); } else { // Otherwise, check all filtered items setChecked([ ...checked, - ...sourceItemKeys.filter(item => !checked.includes(item)) + ...itemKeys.filter(item => !checked.includes(item)) ]); } }; - /** - * Handle check all items in the target list - */ - const handleCheckAllTarget = () => { - // Get the items depending on if there is a search - const targetItemsToCheck = targetFilter - ? filteredTargetItems - : allTargetItems; - - // Get the items keys - const targetItemKeys = targetItemsToCheck.map(item => filterKey(item)); - - if (targetItemKeys.every(item => checked.includes(item))) { - // If all filtered items are checked, uncheck only those - setChecked(checked.filter(item => !targetItemKeys.includes(item))); - } else { - // Otherwise, check all filtered items - setChecked([ - ...checked, - ...targetItemKeys.filter(item => !checked.includes(item)) - ]); - } - }; + // Usage for source + const handleCheckAllSource = () => + handleCheckAll({ + allItems: allSourceItems, + filteredItems: filteredSourceItems, + isFiltered: !!sourceFilter // Convert filter string to boolean + }); + + // Usage for target + const handleCheckAllTarget = () => + handleCheckAll({ + allItems: allTargetItems, + filteredItems: filteredTargetItems, + isFiltered: !!targetFilter // Convert filter string to boolean + }); /** * Determine with items are an array of objects or strings diff --git a/src/TransferList/TransferList.types.ts b/src/TransferList/TransferList.types.ts index 446d0456..b3f3769a 100644 --- a/src/TransferList/TransferList.types.ts +++ b/src/TransferList/TransferList.types.ts @@ -1,73 +1,91 @@ -export type TransferListItem = { - /** - * Unique key - */ - key: string; - /** - * Primary label rendered in an item - */ - primaryLabel: string; - /** - * Secondary label rendered in an item - */ - secondaryLabel?: string; -}; - -export type TransferListProps = { - /** - * Array of default keys to initialise the right hand side for uncontrolled use - */ - defaultSelectedItems?: string[]; - /** - * Array of Items. - */ - items: string[] | TransferListItem[]; - /** - * Callback fired when the items are transferred. - */ - onChange?: (value: string[] | TransferListItem[]) => void; - /** - * Array of keys for the items on the right side for controlled use - */ - selectedItems?: string[]; - /** - * Source list label - */ - sourceListLabel?: string; - /** - * Target list label - */ - targetListLabel?: string; -}; - -export type SingleListProps = { - /** - * Checked items - */ - checked: string[]; - /** - * ID of the list is also used to get the list aria label - */ - id: string; - /** - * Item array - */ - items: { - /** - * Unique identifier - */ - key: string; - /** - * Primary label of the item - */ - primaryLabel: string; - /** - * Secondary label of the item - */ - secondaryLabel?: string; - }[]; - /** - * Toggle function - */ - handleToggle: (key: string) => void; -}; +export type TransferListItem = { + /** + * Unique key + */ + key: string; + /** + * Primary label rendered in an item + */ + primaryLabel: string; + /** + * Secondary label rendered in an item + */ + secondaryLabel?: string; +}; + +export type TransferListProps = { + /** + * Array of default keys to initialise the right hand side for uncontrolled use + */ + defaultSelectedItems?: string[]; + /** + * Array of Items. + */ + items: string[] | TransferListItem[]; + /** + * Callback fired when the items are transferred. + */ + onChange?: (value: string[] | TransferListItem[]) => void; + /** + * Array of keys for the items on the right side for controlled use + */ + selectedItems?: string[]; + /** + * Source list label + */ + sourceListLabel?: string; + /** + * Target list label + */ + targetListLabel?: string; +}; + +export type SingleListProps = { + /** + * Checked items + */ + checked: string[]; + /** + * ID of the list is also used to get the list aria label + */ + id: string; + /** + * Item array + */ + items: { + /** + * Unique identifier + */ + key: string; + /** + * Primary label of the item + */ + primaryLabel: string; + /** + * Secondary label of the item + */ + secondaryLabel?: string; + }[]; + /** + * Toggle function + */ + handleToggle: (key: string) => void; +}; + +/** + * Type representing the check state for the transfer list. + */ +export type HandleCheckProps = { + /** + * Indicates whether the list is filtered. + */ + isFiltered: boolean; + /** + * Array of all items in the list (can be strings or TransferListItem objects). + */ + allItems: (TransferListItem | string)[]; + /** + * Array of filtered items based on the search input. + */ + filteredItems: (TransferListItem | string)[]; +}; From 1c3a63389836870f48701b166573e807bfa2f5be Mon Sep 17 00:00:00 2001 From: IvanLazarov-TVM Date: Wed, 5 Feb 2025 15:57:28 +0200 Subject: [PATCH 4/4] Added another test to check simultaneously checked --- src/TransferList/TransferList.test.tsx | 69 ++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/TransferList/TransferList.test.tsx b/src/TransferList/TransferList.test.tsx index 57b738ba..e25faac2 100644 --- a/src/TransferList/TransferList.test.tsx +++ b/src/TransferList/TransferList.test.tsx @@ -958,4 +958,73 @@ describe("TransferList", () => { expect(screen.getByText("3 selected")).toBeTruthy(); expect(selectAllTarget).toHaveProperty("checked", true); }); + + test("select two items from each list independently", async () => { + // array with more items + const itemsArray = [ + { key: "Apples", primaryLabel: "Apples" }, + { key: "Pears", primaryLabel: "Pears", secondaryLabel: "Conference" }, + { key: "Oranges", primaryLabel: "Oranges" }, + { key: "Pineapple", primaryLabel: "Pineapple" } + ]; + + // render the component + const user = userEvent.setup(); + + render( + + ); + + // get source and target lists + const sourceList = screen.getByLabelText("Source List Label"); + const targetList = screen.getByLabelText("Target List Label"); + + // verify source list has items + const sourceItems = within(sourceList).getAllByRole("listitem"); + expect(sourceItems.length).toBe(4); + + // select first two items in the source list + const sourceCheckboxes = within(sourceList).getAllByRole( + "checkbox" + ) as HTMLInputElement[]; + await user.click(sourceCheckboxes[0]); + await user.click(sourceCheckboxes[1]); + + // verify two items are selected in source + expect(screen.getByText("2 selected")).toBeTruthy(); + + // move selected items to target list + const transferButton = screen.getByLabelText("transfer to target list"); + await user.click(transferButton); + + // verify target list now has 2 items + const targetItems = within(targetList).getAllByRole("listitem"); + expect(targetItems.length).toBe(2); + + // ensure source still has the remaining items + const updatedSourceItems = within(sourceList).getAllByRole("listitem"); + expect(updatedSourceItems.length).toBe(2); + + // select first two items in the source list again + const sourceCheckboxesAgain = within(sourceList).getAllByRole( + "checkbox" + ) as HTMLInputElement[]; + await user.click(sourceCheckboxesAgain[0]); + await user.click(sourceCheckboxesAgain[1]); + + // select first two items in the target list + const targetCheckboxes = within(targetList).getAllByRole( + "checkbox" + ) as HTMLInputElement[]; + await user.click(targetCheckboxes[0]); + await user.click(targetCheckboxes[1]); + + // verify two items are selected in source and in target + const selectedItems = screen.getAllByText("2 selected"); + expect(selectedItems.length).toBe(2); + }); });