Skip to content

Commit

Permalink
feat: Virtualised list for search results (#3722)
Browse files Browse the repository at this point in the history
  • Loading branch information
DafyddLlyr authored Oct 1, 2024
1 parent 45c295e commit 48213f7
Show file tree
Hide file tree
Showing 12 changed files with 351 additions and 175 deletions.
1 change: 1 addition & 0 deletions editor.planx.uk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"react-navi-helmet-async": "^0.15.0",
"react-toastify": "^9.1.3",
"react-use": "^17.5.0",
"react-virtuoso": "^4.10.4",
"reconnecting-websocket": "^4.4.0",
"rxjs": "^7.8.1",
"scroll-into-view-if-needed": "^3.1.0",
Expand Down
14 changes: 14 additions & 0 deletions editor.planx.uk/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
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 { ExternalPortalList } from "./ExternalPortalList";
import Search from ".";
import { flow } from "./mocks/simple";
import { VirtuosoWrapper } from "./testUtils";

vi.mock("react-navi", () => ({
useNavigation: () => ({
navigate: vi.fn(),
}),
}));

const { getState, setState } = useStore;

let initialState: FullStore;

beforeAll(() => (initialState = getState()));

beforeEach(() => setState({ flow }));
afterEach(() => act(() => setState(initialState)));

const externalPortals: FullStore["externalPortals"] = {
Expand All @@ -19,32 +31,83 @@ const externalPortals: FullStore["externalPortals"] = {
};

it("does not display if there are no external portals in the flow", () => {
const { container } = setup(<ExternalPortalList />);
const { queryByTestId } = setup(
<VirtuosoWrapper>
<Search />
</VirtuosoWrapper>,
);

expect(queryByTestId("searchExternalPortalList")).not.toBeInTheDocument();
});

expect(container).toBeEmptyDOMElement();
it("does not display if there is no search term provided", () => {
act(() => setState({ externalPortals }));

const { queryByTestId } = setup(
<VirtuosoWrapper>
<Search />
</VirtuosoWrapper>,
);

expect(queryByTestId("searchExternalPortalList")).not.toBeInTheDocument();
});

it("displays a list of external portals if present in the flow", () => {
it("displays a list of external portals if present in the flow, and a search term is provided", async () => {
act(() => setState({ externalPortals }));
const { container, getAllByRole } = setup(<ExternalPortalList />);

expect(container).not.toBeEmptyDOMElement();
expect(getAllByRole("listitem")).toHaveLength(2);
const { findByTestId, getByText, getByLabelText, user } = setup(
<VirtuosoWrapper>
<Search />
</VirtuosoWrapper>,
);

const searchInput = getByLabelText("Search this flow and internal portals");
user.type(searchInput, "ind");

const externalPortalList = await waitFor(() =>
findByTestId("searchExternalPortalList"),
);

expect(externalPortalList).toBeDefined();
expect(getByText(/portalOne/)).toBeInTheDocument();
expect(getByText(/portalTwo/)).toBeInTheDocument();
});

it("allows users to navigate to the external portals", () => {
it("allows users to navigate to the external portals", async () => {
act(() => setState({ externalPortals }));
const { container, getAllByRole } = setup(<ExternalPortalList />);

expect(container).not.toBeEmptyDOMElement();
const [first, second] = getAllByRole("link") as HTMLAnchorElement[];
const { getAllByRole, getByLabelText, user } = setup(
<VirtuosoWrapper>
<Search />
</VirtuosoWrapper>,
);

const searchInput = getByLabelText("Search this flow and internal portals");
user.type(searchInput, "ind");

const [first, second] = await waitFor(
() => 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(<ExternalPortalList />);

const { container, getByLabelText, user, findByTestId } = setup(
<VirtuosoWrapper>
<Search />
</VirtuosoWrapper>,
);

const searchInput = getByLabelText("Search this flow and internal portals");
user.type(searchInput, "ind");

await waitFor(() =>
expect(findByTestId("searchExternalPortalList")).toBeDefined(),
);

const results = await axe(container);
expect(results).toHaveNoViolations();
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { styled } from "@mui/material/styles";
import Typography from "@mui/material/Typography";
import { useStore } from "pages/FlowEditor/lib/store";
import React from "react";
import { Components } from "react-virtuoso";

import { Context, Data } from ".";

export const Root = styled(List)(({ theme }) => ({
color: theme.palette.text.primary,
Expand All @@ -14,14 +17,21 @@ export const Root = styled(List)(({ theme }) => ({
border: `1px solid ${theme.palette.border.light}`,
}));

export const ExternalPortalList: React.FC = () => {
export const ExternalPortalList: Components<Data, Context>["Footer"] = ({
context,
}) => {
// Only display if there are external portals
const externalPortals = useStore((state) => state.externalPortals);
const hasExternalPortals = Object.keys(externalPortals).length;

if (!hasExternalPortals) return null;

// Only display if a search is in progress
if (!context)
throw Error("Virtuoso context must be provided to ExternalPortalList");
if (!context.results.length && !context.formik.values.pattern) return null;

return (
<Box pt={2}>
<Box mx={3} pb={2} data-testid="searchExternalPortalList">
<Typography variant="body1" mb={2}>
Your service also contains the following external portals, which have
not been searched:
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { waitFor } from "@testing-library/react";
import { useStore } from "pages/FlowEditor/lib/store";
import React from "react";
import { setup } from "testUtils";
import { vi } from "vitest";

import Search from ".";
import { flow } from "./mocks/simple";
import { VirtuosoWrapper } from "./testUtils";

vi.mock("react-navi", () => ({
useNavigation: () => ({
navigate: vi.fn(),
}),
}));

beforeAll(() => useStore.setState({ flow }));

it("Displays a warning if no results are returned", async () => {
const { getByLabelText, getByText, getByRole, user } = setup(
<VirtuosoWrapper>
<Search />
</VirtuosoWrapper>,
);

const searchInput = getByLabelText("Search this flow and internal portals");
user.type(searchInput, "Timbuktu");

await waitFor(() =>
expect(getByText("No matches found")).toBeInTheDocument(),
);
expect(getByRole("list")).toBeEmptyDOMElement();
});

it("Displays the count for a single result", async () => {
const { getByLabelText, getByText, getAllByRole, getByRole, user } = setup(
<VirtuosoWrapper>
<Search />
</VirtuosoWrapper>,
);

const searchInput = getByLabelText("Search this flow and internal portals");
user.type(searchInput, "Spain");

await waitFor(() => expect(getByText("1 result:")).toBeInTheDocument());
expect(getByRole("list")).not.toBeEmptyDOMElement();
expect(getAllByRole("listitem")).toHaveLength(1);
});

it("Displays the count for multiple results", async () => {
const { getByText, getByRole, getAllByRole, getByLabelText, user } = setup(
<VirtuosoWrapper>
<Search />
</VirtuosoWrapper>,
);

const searchInput = getByLabelText("Search this flow and internal portals");
// Matches "India" and "Indonesia"
user.type(searchInput, "Ind");

await waitFor(() => expect(getByText("2 results:")).toBeInTheDocument());
expect(getByRole("list")).not.toBeEmptyDOMElement();
expect(getAllByRole("listitem")).toHaveLength(2);
});
Loading

0 comments on commit 48213f7

Please sign in to comment.