Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CRDCDH-1500 Create Approved Study List Page #468

Merged
merged 32 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a50c94f
Update approved study types and queries
Alejandro-Vega Sep 3, 2024
8fef084
Add route for studies and controller
Alejandro-Vega Sep 3, 2024
5f5c2f0
Created an approved studies list context
Alejandro-Vega Sep 3, 2024
a71bb8e
Update navbar to include new manage studies
Alejandro-Vega Sep 3, 2024
c25b450
Created init list view for studies and updated organization view
Alejandro-Vega Sep 3, 2024
50bcd56
Merge branch 'CRDCDH-1592' of https://github.com/CBIIT/crdc-datahub-u…
Alejandro-Vega Sep 9, 2024
a17e67e
Add dbGaPID to listApprovedStudies query
Alejandro-Vega Sep 11, 2024
3679263
Update listApprovedStudies query and types
Alejandro-Vega Sep 11, 2024
fbea197
Update StudiesController
Alejandro-Vega Sep 11, 2024
dc1e211
Update studies list view to include filters and handling fetching app…
Alejandro-Vega Sep 11, 2024
5e59265
Remove unused context
Alejandro-Vega Sep 11, 2024
e7bfa59
Merge branch '3.1.0' of https://github.com/CBIIT/crdc-datahub-ui into…
Alejandro-Vega Sep 16, 2024
91c073d
Update type name, and add openAccess to the listApprovedStudies param…
Alejandro-Vega Sep 18, 2024
20c28a6
Remove console log
Alejandro-Vega Sep 18, 2024
47d3bf3
Merge branch '3.1.0' of https://github.com/CBIIT/crdc-datahub-ui into…
Alejandro-Vega Sep 18, 2024
8ab60f0
Merge branch '3.1.0' into CRDCDH-1500
Alejandro-Vega Sep 26, 2024
c050b78
Fix return properties for listApprovedStudies query and update usage
Alejandro-Vega Sep 26, 2024
ff08ffa
Created utility function for formatting access types in the table. Al…
Alejandro-Vega Sep 26, 2024
b3f2794
Update table column formatting and query variables
Alejandro-Vega Sep 26, 2024
2feefc4
Adjusted search param filters and column
Alejandro-Vega Sep 27, 2024
bd6ed59
Merge branch '3.1.0' of https://github.com/CBIIT/crdc-datahub-ui into…
Alejandro-Vega Sep 30, 2024
06a6d36
Removed unecessary guard
Alejandro-Vega Sep 30, 2024
9c9daee
Remove comment
Alejandro-Vega Sep 30, 2024
2d86996
Split filters logic into a separate testable component. Added initial…
Alejandro-Vega Sep 30, 2024
2d94cc8
Add more test coverage
Alejandro-Vega Sep 30, 2024
bebac0a
Update approved study sorting to server-side
Alejandro-Vega Sep 30, 2024
ad6ed14
Move type to different location
Alejandro-Vega Sep 30, 2024
c8418d9
Disabled sorting for Access Type column
Alejandro-Vega Sep 30, 2024
1df57de
Fixed searchParams updating too much causing crash
Alejandro-Vega Sep 30, 2024
9b3be3a
Merge branch '3.1.0' into CRDCDH-1500
Alejandro-Vega Oct 1, 2024
7d42ceb
Added return type to function
Alejandro-Vega Oct 1, 2024
5291a12
Merge branch 'CRDCDH-1500' of https://github.com/CBIIT/crdc-datahub-u…
Alejandro-Vega Oct 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
381 changes: 381 additions & 0 deletions src/components/AdminPortal/Studies/ApprovedStudyFilters.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,381 @@
import React, { FC } from "react";
import { fireEvent, render, waitFor, within } from "@testing-library/react";
import { MemoryRouter, MemoryRouterProps } from "react-router-dom";
import userEvent from "@testing-library/user-event";
import { axe } from "jest-axe";
import { MockedProvider, MockedResponse } from "@apollo/client/testing";
import ApprovedStudyFilters from "./ApprovedStudyFilters";
import { SearchParamsProvider, useSearchParamsContext } from "../../Contexts/SearchParamsContext";

type ParentProps = {
mocks?: MockedResponse[];
initialEntries?: MemoryRouterProps["initialEntries"];
children: React.ReactNode;
};

const TestParent: FC<ParentProps> = ({ mocks, initialEntries = ["/"], children }: ParentProps) => (
<MockedProvider mocks={mocks}>
<MemoryRouter initialEntries={initialEntries}>
<SearchParamsProvider>{children}</SearchParamsProvider>
</MemoryRouter>
</MockedProvider>
);

describe("ApprovedStudyFilters Component", () => {
afterEach(() => {
jest.clearAllMocks();
jest.useRealTimers();
});

it("renders without crashing", () => {
const { getByTestId } = render(
<TestParent>
<ApprovedStudyFilters />
</TestParent>
);
expect(getByTestId("approved-study-filters")).toBeInTheDocument();
});

it("has no accessibility violations", async () => {
const { container } = render(
<TestParent>
<ApprovedStudyFilters />
</TestParent>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});

it("renders all input fields correctly", () => {
const { getByTestId } = render(
<TestParent>
<ApprovedStudyFilters />
</TestParent>
);
expect(getByTestId("study-input")).toBeInTheDocument();
expect(getByTestId("dbGaPID-input")).toBeInTheDocument();
expect(getByTestId("accessType-select")).toBeInTheDocument();
});

it("allows users to select an access type", async () => {
const mockOnChange = jest.fn();
const { getByTestId } = render(
<TestParent>
<ApprovedStudyFilters onChange={mockOnChange} />
</TestParent>
);

const accessTypeSelect = within(getByTestId("accessType-select")).getByRole("button");

userEvent.click(accessTypeSelect);

await waitFor(() => {
const muiSelectList = within(getByTestId("accessType-select")).getByRole("listbox", {
hidden: true,
});
expect(within(muiSelectList).getByTestId("accessType-option-Controlled")).toBeInTheDocument();
expect(within(muiSelectList).getByTestId("accessType-option-All")).toBeInTheDocument();
expect(within(muiSelectList).getByTestId("accessType-option-Open")).toBeInTheDocument();
});

userEvent.click(getByTestId("accessType-option-Controlled"));

await waitFor(() => {
expect(mockOnChange).toHaveBeenCalledWith(
expect.objectContaining({
accessType: "Controlled",
})
);
});
});

it("sets accessType correctly when selecting 'Open'", async () => {
const mockOnChange = jest.fn();

const { getByTestId } = render(
<TestParent>
<ApprovedStudyFilters onChange={mockOnChange} />
</TestParent>
);

const accessTypeSelect = within(getByTestId("accessType-select")).getByRole("button");

userEvent.click(accessTypeSelect);

await waitFor(() => {
const muiSelectList = within(getByTestId("accessType-select")).getByRole("listbox", {
hidden: true,
});
expect(within(muiSelectList).getByTestId("accessType-option-Controlled")).toBeInTheDocument();
expect(within(muiSelectList).getByTestId("accessType-option-All")).toBeInTheDocument();
expect(within(muiSelectList).getByTestId("accessType-option-Open")).toBeInTheDocument();
});

userEvent.click(getByTestId("accessType-option-Open"));

await waitFor(() => {
expect(mockOnChange).toHaveBeenCalledWith(
expect.objectContaining({
accessType: "Open",
})
);
});
});

it("deletes 'accessType' from searchParams when accessTypeFilter is set to 'All'", async () => {
const ShowSearchParams = () => {
const { searchParams } = useSearchParamsContext();
return <div data-testid="search-params">{searchParams.toString()}</div>;
};

const mockOnChange = jest.fn();

const { getByTestId } = render(
<TestParent initialEntries={["/?accessType=Controlled"]}>
<ApprovedStudyFilters onChange={mockOnChange} />
<ShowSearchParams />
</TestParent>
);

await waitFor(() => {
expect(getByTestId("search-params")).toHaveTextContent("accessType=Controlled");
});

const accessTypeSelect = within(getByTestId("accessType-select")).getByRole("button");

userEvent.click(accessTypeSelect);

await waitFor(() => {
const muiSelectList = within(getByTestId("accessType-select")).getByRole("listbox", {
hidden: true,
});
expect(within(muiSelectList).getByTestId("accessType-option-Controlled")).toBeInTheDocument();
expect(within(muiSelectList).getByTestId("accessType-option-All")).toBeInTheDocument();
expect(within(muiSelectList).getByTestId("accessType-option-Open")).toBeInTheDocument();
});

userEvent.click(getByTestId("accessType-option-All"));

// Wait for 'accessType' to be deleted from searchParams
await waitFor(() => {
expect(getByTestId("search-params")).not.toHaveTextContent("accessType=All");
expect(getByTestId("search-params")).not.toHaveTextContent("accessType=Controlled");
});

// Ensure 'accessType' is removed from searchParams
expect(getByTestId("search-params")).not.toContain("accessType=");
});

it("allows users to type into the study input", async () => {
const { getByTestId } = render(
<TestParent>
<ApprovedStudyFilters />
</TestParent>
);
const studyInput = getByTestId("study-input");

userEvent.type(studyInput, "Cancer Study");

expect(studyInput).toHaveValue("Cancer Study");
});

it("allows users to type into the dbGaPID input", async () => {
const { getByTestId } = render(
<TestParent>
<ApprovedStudyFilters />
</TestParent>
);
const dbGaPIDInput = getByTestId("dbGaPID-input");

userEvent.type(dbGaPIDInput, "DB12345");

expect(dbGaPIDInput).toHaveValue("DB12345");
});

it("debounces input changes for study and dbGaPID fields", async () => {
jest.useFakeTimers();
const mockOnChange = jest.fn();
const { getByTestId } = render(
<TestParent>
<ApprovedStudyFilters onChange={mockOnChange} />
</TestParent>
);

expect(mockOnChange).toHaveBeenCalledWith({
study: "",
dbGaPID: "",
accessType: "All",
});

const studyInput = getByTestId("study-input");
const dbGaPIDInput = getByTestId("dbGaPID-input");

userEvent.type(studyInput, "Can");
userEvent.type(dbGaPIDInput, "DB1");

// Advance timers by less than debounce time (500ms)
jest.advanceTimersByTime(300);
expect(mockOnChange).not.toHaveBeenCalledWith(
expect.objectContaining({
study: "Can",
dbGaPID: "DB1",
})
);

// Advance timers to exceed debounce time
jest.advanceTimersByTime(300);

await waitFor(() => {
expect(mockOnChange).toHaveBeenCalledWith({
study: "Can",
dbGaPID: "DB1",
accessType: "All",
});
});

jest.useRealTimers();
});

it("handles empty input fields correctly", async () => {
const mockOnChange = jest.fn();
const { getByTestId } = render(
<TestParent>
<ApprovedStudyFilters onChange={mockOnChange} />
</TestParent>
);

const studyInput = getByTestId("study-input");
const dbGaPIDInput = getByTestId("dbGaPID-input");

fireEvent.change(studyInput, { target: { value: "" } });
fireEvent.change(dbGaPIDInput, { target: { value: "" } });

await waitFor(() => {
expect(mockOnChange).toHaveBeenCalledWith({
study: "",
dbGaPID: "",
accessType: "All",
});
});
});

it("prevents infinite loops by ensuring setSearchParams is called appropriately", async () => {
jest.useFakeTimers();
const mockOnChange = jest.fn();
const { getByTestId } = render(
<TestParent>
<ApprovedStudyFilters onChange={mockOnChange} />
</TestParent>
);

const studyInput = getByTestId("study-input");

userEvent.clear(studyInput);
userEvent.type(studyInput, "Test Study");

// Advance timers to trigger debounce
jest.advanceTimersByTime(500);

await waitFor(() => {
expect(mockOnChange).toHaveBeenCalledTimes(2);
expect(mockOnChange).toHaveBeenCalledWith({
study: "Test Study",
dbGaPID: "",
accessType: "All",
});
});

// Ensure no additional calls are made
jest.advanceTimersByTime(500);
expect(mockOnChange).toHaveBeenCalledTimes(2);

jest.useRealTimers();
});

it("updates dbGaPID input when searchParams dbGaPID is different", async () => {
const mockOnChange = jest.fn();
const { getByTestId } = render(
<TestParent initialEntries={["/test?dbGaPID=DB123"]}>
<ApprovedStudyFilters onChange={mockOnChange} />
</TestParent>
);

const dbGaPIDInput = getByTestId("dbGaPID-input");

await waitFor(() => {
expect(dbGaPIDInput).toHaveValue("DB123");
});

expect(mockOnChange).toHaveBeenCalledWith({
study: "",
dbGaPID: "DB123",
accessType: "All",
});
});

it("updates accessType dropdown when searchParams accessType is different", async () => {
const mockOnChange = jest.fn();

const { getByTestId } = render(
<TestParent initialEntries={["/test?accessType=Controlled"]}>
<ApprovedStudyFilters onChange={mockOnChange} />
</TestParent>
);

const accessTypeSelect = getByTestId("accessType-select");

await waitFor(() => {
expect(accessTypeSelect).toHaveTextContent("Controlled");
});

expect(mockOnChange).toHaveBeenCalledWith({
study: "",
dbGaPID: "",
accessType: "Controlled",
});
});

it("handles accessTypeFilter being 'All' correctly when study equals studyFilter", async () => {
const mockOnChange = jest.fn();

const { getByTestId } = render(
<TestParent initialEntries={["/?study=Study1&accessType=All"]}>
<ApprovedStudyFilters onChange={mockOnChange} />
</TestParent>
);

const studyInput = getByTestId("study-input");
const accessTypeSelect = getByTestId("accessType-select");

await waitFor(() => {
expect(studyInput).toHaveValue("Study1");
});

expect(accessTypeSelect).toHaveTextContent("All");

expect(mockOnChange).toHaveBeenCalledWith({
study: "Study1",
dbGaPID: "",
accessType: "All",
});
});

it("handles invalid accessTypeFilter value in searchParams correctly", async () => {
const mockOnChange = jest.fn();

const { getByTestId } = render(
<TestParent initialEntries={["/?study=Study1&accessType=invalid-access-type"]}>
<ApprovedStudyFilters onChange={mockOnChange} />
</TestParent>
);

const accessTypeSelect = getByTestId("accessType-select");

expect(accessTypeSelect).toHaveTextContent("All");
expect(mockOnChange).toHaveBeenCalledWith({
study: "Study1",
dbGaPID: "",
accessType: "All",
});
});
});
Loading
Loading