diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/ExternalPortalList.test.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/ExternalPortalList.test.tsx new file mode 100644 index 0000000000..d5b8cc2e5b --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/ExternalPortalList.test.tsx @@ -0,0 +1,50 @@ +import { FullStore, useStore } from "pages/FlowEditor/lib/store"; +import React from "react"; +import { act } from "react-dom/test-utils"; +import { setup } from "testUtils"; +import { axe } from "vitest-axe"; + +import { ExternalPortalList } from "./ExternalPortalList"; + +const { getState, setState } = useStore; + +let initialState: FullStore; + +beforeAll(() => (initialState = getState())); +afterEach(() => act(() => setState(initialState))); + +const externalPortals: FullStore["externalPortals"] = { + abc: { name: "Portal 1", href: "myTeam/portalOne" }, + def: { name: "Portal 2", href: "myTeam/portalTwo" }, +}; + +it("does not display if there are no external portals in the flow", () => { + const { container } = setup(); + + expect(container).toBeEmptyDOMElement(); +}); + +it("displays a list of external portals if present in the flow", () => { + act(() => setState({ externalPortals })); + const { container, getAllByRole } = setup(); + + expect(container).not.toBeEmptyDOMElement(); + expect(getAllByRole("listitem")).toHaveLength(2); +}); + +it("allows users to navigate to the external portals", () => { + act(() => setState({ externalPortals })); + const { container, getAllByRole } = setup(); + + expect(container).not.toBeEmptyDOMElement(); + const [first, second] = getAllByRole("link") as HTMLAnchorElement[]; + expect(first).toHaveAttribute("href", "../myTeam/portalOne"); + expect(second).toHaveAttribute("href", "../myTeam/portalTwo"); +}); + +it("should not have any accessibility violations on initial load", async () => { + act(() => setState({ externalPortals })); + const { container } = setup(); + const results = await axe(container); + expect(results).toHaveNoViolations(); +}); diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/Headline.test.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/Headline.test.tsx new file mode 100644 index 0000000000..b026dcad2d --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/Headline.test.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { setup } from "testUtils"; +import { FONT_WEIGHT_BOLD } from "theme"; +import { axe } from "vitest-axe"; + +import { Headline } from "./Headline"; + +const sampleText = "The quick fox jumps..."; +const foxIndices: [number, number][] = [[10, 13]]; + +const DEFAULT_FONT_WEIGHT = "400"; + +it("displays matches from the headline in bold", () => { + const { getByText } = setup( + , + ); + + // Input text is split into characters in order to highlight a substring + const tStyle = window.getComputedStyle(getByText("T")); + const hStyle = window.getComputedStyle(getByText("h")); + const eStyle = window.getComputedStyle(getByText("e")); + + // Non matching text is not in bold + expect(tStyle.fontWeight).toEqual(DEFAULT_FONT_WEIGHT); + expect(hStyle.fontWeight).toEqual(DEFAULT_FONT_WEIGHT); + expect(eStyle.fontWeight).toEqual(DEFAULT_FONT_WEIGHT); + + const fStyle = window.getComputedStyle(getByText("f")); + const oStyle = window.getComputedStyle(getByText("o")); + const xStyle = window.getComputedStyle(getByText("x")); + + // Matching text is in bold + expect(fStyle.fontWeight).toEqual(FONT_WEIGHT_BOLD); + expect(oStyle.fontWeight).toEqual(FONT_WEIGHT_BOLD); + expect(xStyle.fontWeight).toEqual(FONT_WEIGHT_BOLD); +}); + +it("should not have any accessibility violations on initial load", async () => { + const { container } = setup( + , + ); + const results = await axe(container); + expect(results).toHaveNoViolations(); +}); diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/NodeSearchResults.test.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/NodeSearchResults.test.tsx new file mode 100644 index 0000000000..042d9b4755 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/NodeSearchResults.test.tsx @@ -0,0 +1,39 @@ +import { useStore } from "pages/FlowEditor/lib/store"; +import React from "react"; +import { setup } from "testUtils"; +import { axe } from "vitest-axe"; + +import { flow, results } from "./mocks/simple"; +import { NodeSearchResults } from "./NodeSearchResults"; + +beforeAll(() => useStore.setState({ flow })); + +it("Displays a warning if no results are returned", () => { + const { getByText, getByRole } = setup(); + expect(getByText("No matches found")).toBeInTheDocument(); + expect(getByRole("list")).toBeEmptyDOMElement(); +}); + +it("Displays the count for a single result", () => { + const { getByText, getByRole, getAllByRole } = setup( + , + ); + expect(getByText("1 result:")).toBeInTheDocument(); + expect(getByRole("list")).not.toBeEmptyDOMElement(); + expect(getAllByRole("listitem")).toHaveLength(1); +}); + +it("Displays the count for multiple results", () => { + const { getByText, getByRole, getAllByRole } = setup( + , + ); + expect(getByText("2 results:")).toBeInTheDocument(); + expect(getByRole("list")).not.toBeEmptyDOMElement(); + expect(getAllByRole("listitem")).toHaveLength(2); +}); + +it("should not have any accessibility violations on initial load", async () => { + const { container } = setup(); + const axeResults = await axe(container); + expect(axeResults).toHaveNoViolations(); +}); diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/index.test.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/index.test.tsx new file mode 100644 index 0000000000..a8b6e0bc74 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/index.test.tsx @@ -0,0 +1,94 @@ +import * as planxCore from "@opensystemslab/planx-core"; +import { waitFor } from "@testing-library/react"; +import { FullStore, useStore } from "pages/FlowEditor/lib/store"; +import React from "react"; +import { act } from "react-dom/test-utils"; +import { setup } from "testUtils"; +import { vi } from "vitest"; +import { axe } from "vitest-axe"; + +import Search from "."; +import { flow } from "./mocks/simple"; + +const { setState, getState } = useStore; + +let initialState: FullStore; + +beforeAll(() => (initialState = getState())); + +beforeEach(() => setState({ flow })); +afterEach(() => act(() => setState(initialState))); + +vi.mock("@opensystemslab/planx-core", async (originalModule) => { + const actualModule = await originalModule(); + return { + ...actualModule, + // Spy on sortFlow while keeping its original implementation + sortFlow: vi.fn(actualModule.sortFlow), + }; +}); + +test("data field checkbox is checked and disabled", () => { + const { getByLabelText } = setup(); + const checkbox = getByLabelText("Search only data fields"); + + expect(checkbox).toBeInTheDocument(); + expect(checkbox).toBeChecked(); + expect(checkbox).toBeDisabled(); +}); + +test("entering a search term displays a series of cards", async () => { + const { user, queryByRole, getByRole, getAllByRole, getByLabelText } = setup( + , + ); + + expect(queryByRole("list")).not.toBeInTheDocument(); + + const searchInput = getByLabelText("Search this flow and internal portals"); + user.type(searchInput, "ind"); + + await waitFor(() => expect(getByRole("list")).toBeInTheDocument()); + await waitFor(() => expect(getAllByRole("listitem")).toHaveLength(2)); +}); + +test.todo("cards link to their associated nodes", async () => { + const { user, getAllByRole, getByLabelText } = setup(); + + const searchInput = getByLabelText("Search this flow and internal portals"); + user.type(searchInput, "ind"); + + await waitFor(() => expect(getAllByRole("listitem")).toHaveLength(2)); + + const [first, second] = getAllByRole("listitem"); + // TODO! + expect(first).toHaveAttribute("href", "link to tR9tdaWOvF (India)"); + expect(second).toHaveAttribute("href", "link to tvUxd2IoPo (Indonesia)"); +}); + +it("orderedFlow is set in the store on render of Search", async () => { + expect(getState().orderedFlow).toBeUndefined(); + + setup(); + + expect(getState().orderedFlow).toBeDefined(); +}); + +test("setOrderedFlow is only called once on initial render", async () => { + const sortFlowSpy = vi.spyOn(planxCore, "sortFlow"); + expect(sortFlowSpy).not.toHaveBeenCalled(); + + const { user, getAllByRole, getByLabelText } = setup(); + + const searchInput = getByLabelText("Search this flow and internal portals"); + user.type(searchInput, "ind"); + + await waitFor(() => expect(getAllByRole("listitem")).toHaveLength(2)); + + expect(sortFlowSpy).toHaveBeenCalledTimes(1); +}); + +it("should not have any accessibility violations on initial load", async () => { + const { container } = setup(); + const results = await axe(container); + expect(results).toHaveNoViolations(); +}); diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/index.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/index.tsx index a7f9a825f5..9aaecb84de 100644 --- a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/index.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/index.tsx @@ -51,6 +51,7 @@ const Search: React.FC = () => { Search this flow and internal portals { diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/simple.ts b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/simple.ts new file mode 100644 index 0000000000..275c397f34 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/components/Sidebar/Search/mocks/simple.ts @@ -0,0 +1,66 @@ +import { FlowGraph, IndexedNode } from "@opensystemslab/planx-core/types"; +import { SearchResults } from "hooks/useSearch"; + +export const flow: FlowGraph = { + _root: { + edges: ["Ej0xpn4l8u"], + }, + Ej0xpn4l8u: { + type: 100, + data: { + fn: "country", + text: "Pick a country", + }, + edges: ["VhSydY2fTe", "tR9tdaWOvF", "tvUxd2IoPo"], + }, + VhSydY2fTe: { + type: 200, + data: { + text: "Spain", + val: "spain", + }, + }, + tR9tdaWOvF: { + type: 200, + data: { + text: "India", + val: "india", + }, + }, + tvUxd2IoPo: { + type: 200, + data: { + text: "Indonesia", + val: "indonesia", + }, + }, +}; + +export const results: SearchResults = [ + { + item: { + id: "tR9tdaWOvF", + parentId: "Ej0xpn4l8u", + type: 200, + data: { + text: "India", + val: "india", + }, + }, + key: "data.val", + matchIndices: [[0, 2]], + }, + { + item: { + id: "tvUxd2IoPo", + parentId: "Ej0xpn4l8u", + type: 200, + data: { + text: "Indonesia", + val: "indonesia", + }, + }, + key: "data.val", + matchIndices: [[0, 2]], + }, +];