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-881 Data Submission > Submitted Data tab #317

Merged
merged 14 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions package-lock.json

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

35 changes: 25 additions & 10 deletions src/components/DataSubmissions/GenericTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
styled,
} from "@mui/material";
import { CSSProperties, ElementType, forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react";
import { useAuthContext } from "../Contexts/AuthContext";
import PaginationActions from "./PaginationActions";
import SuspenseLoader from '../SuspenseLoader';

Expand All @@ -24,8 +23,7 @@ const StyledTableContainer = styled(TableContainer)({
border: "1px solid #6CACDA",
marginBottom: "25px",
position: "relative",
overflowX: "auto",
overflowY: "hidden",
overflow: "hidden",
"& .MuiTableRow-root:nth-of-type(2n)": {
background: "#E3EEF9",
},
Expand All @@ -37,6 +35,12 @@ const StyledTableContainer = styled(TableContainer)({
},
});

const StyledTable = styled(Table, { shouldForwardProp: (p) => p !== "horizontalScroll" })<{ horizontalScroll: boolean }>(({ horizontalScroll }) => ({
whiteSpace: horizontalScroll ? "nowrap" : "initial",
display: horizontalScroll ? "block" : "table",
overflowX: horizontalScroll ? "auto" : "initial",
}));

const StyledTableHead = styled(TableHead)({
background: "#4D7C8F",
});
Expand Down Expand Up @@ -126,7 +130,7 @@ export type Order = "asc" | "desc";

export type Column<T> = {
label: string | React.ReactNode;
renderValue: (a: T, user: User) => string | boolean | number | React.ReactNode;
renderValue: (a: T) => string | boolean | number | React.ReactNode;
field?: keyof T;
default?: true;
sortDisabled?: boolean;
Expand All @@ -150,6 +154,7 @@ type Props<T> = {
data: T[];
total: number;
loading?: boolean;
horizontalScroll?: boolean;
noContentText?: string;
defaultOrder?: Order;
defaultRowsPerPage?: number;
Expand All @@ -169,6 +174,7 @@ const GenericTable = <T,>({
data,
total = 0,
loading,
horizontalScroll = false,
noContentText,
defaultOrder = "desc",
defaultRowsPerPage = 10,
Expand All @@ -182,7 +188,6 @@ const GenericTable = <T,>({
onOrderByChange,
onPerPageChange,
}: Props<T>, ref: React.Ref<TableMethods>) => {
const { user } = useAuthContext();
const [order, setOrder] = useState<Order>(defaultOrder);
const [orderBy, setOrderBy] = useState<Column<T>>(
columns.find((c) => c.default) || columns.find((c) => c.field)
Expand Down Expand Up @@ -248,11 +253,15 @@ const GenericTable = <T,>({
return (
<StyledTableContainer {...containerProps}>
{loading && (<SuspenseLoader fullscreen={false} />)}
<Table>
<StyledTable horizontalScroll={horizontalScroll && total > 0}>
<StyledTableHead>
<TableRow>
{columns.map((col: Column<T>) => (
<StyledHeaderCell key={col.label.toString()} sx={col.sx}>
<StyledHeaderCell
key={col.label.toString()}
sx={col.sx}
data-testid={`generic-table-header-${col.label.toString()}`}
>
{col.field && !col.sortDisabled ? (
<TableSortLabel
active={orderBy === col}
Expand Down Expand Up @@ -280,7 +289,7 @@ const GenericTable = <T,>({
<TableRow tabIndex={-1} hover key={itemKey}>
{columns.map((col: Column<T>) => (
<StyledTableCell key={`${itemKey}_${col.label}`}>
{col.renderValue(d, user)}
{col.renderValue(d)}
</StyledTableCell>
))}
</TableRow>
Expand Down Expand Up @@ -313,7 +322,7 @@ const GenericTable = <T,>({
</TableRow>
)}
</TableBody>
</Table>
</StyledTable>
<StyledTablePagination
rowsPerPageOptions={[5, 10, 20, 50]}
component="div"
Expand All @@ -331,7 +340,13 @@ const GenericTable = <T,>({
|| emptyRows > 0
|| loading
}}
SelectProps={{ inputProps: { "aria-label": "rows per page" }, native: true }}
SelectProps={{
inputProps: {
"aria-label": "rows per page",
"data-testid": "generic-table-rows-per-page"
},
native: true,
}}
backIconButtonProps={{ disabled: page === 0 || loading }}
// eslint-disable-next-line react/no-unstable-nested-components
ActionsComponent={(props) => <PaginationActions {...props} AdditionalActions={AdditionalActions} />}
Expand Down
189 changes: 189 additions & 0 deletions src/components/DataSubmissions/SubmittedDataFilters.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { FC } from 'react';
import { render, waitFor, within } from '@testing-library/react';
import UserEvent from '@testing-library/user-event';
import { axe } from 'jest-axe';
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
import { SubmittedDataFilters } from './SubmittedDataFilters';
import { SUBMISSION_STATS, SubmissionStatsResp } from '../../graphql';

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

const TestParent: FC<ParentProps> = ({ mocks, children } : ParentProps) => (
<MockedProvider mocks={mocks} showWarnings>
{children}
</MockedProvider>
);

describe("SubmittedDataFilters cases", () => {
const baseStatistic: SubmissionStatistic = {
nodeName: "",
total: 0,
new: 0,
passed: 0,
warning: 0,
error: 0
};

it("should not have accessibility violations", async () => {
const { container } = render(
<TestParent mocks={[]}>
<SubmittedDataFilters submissionId={undefined} />
</TestParent>
);

expect(await axe(container)).toHaveNoViolations();
});

it("should handle an empty array of node types without errors", async () => {
const _id = "example-empty-results";
const mocks: MockedResponse<SubmissionStatsResp>[] = [{
request: {
query: SUBMISSION_STATS,
variables: { id: _id },
},
result: {
data: {
submissionStats: {
stats: [],
},
},
},
}];

expect(() => render(
<TestParent mocks={mocks}>
<SubmittedDataFilters submissionId={_id} />
</TestParent>
)).not.toThrow();
});

// NOTE: The sorting function `compareNodeStats` is already heavily tested, this is just a sanity check
it("should sort the node types by count in descending order", async () => {
const _id = "example-sorting-by-count-id";
const mocks: MockedResponse<SubmissionStatsResp>[] = [{
request: {
query: SUBMISSION_STATS,
variables: {
id: _id
},
},
result: {
data: {
submissionStats: {
stats: [
{ ...baseStatistic, nodeName: "N-3", total: 1 },
{ ...baseStatistic, nodeName: "N-1", total: 3 },
{ ...baseStatistic, nodeName: "N-2", total: 2 },
],
},
},
},
}];

const { getByTestId } = render(
<TestParent mocks={mocks}>
<SubmittedDataFilters submissionId={_id} />
</TestParent>
);

const muiSelectBox = within(getByTestId("data-content-node-filter")).getByRole("button");

await waitFor(() => {
UserEvent.click(muiSelectBox);

const muiSelectList = within(getByTestId("data-content-node-filter")).getByRole("listbox", { hidden: true });

// The order of the nodes should be N-1 < N-2 < N-3
expect(muiSelectList).toBeInTheDocument();
expect(muiSelectList.innerHTML.search("N-1")).toBeLessThan(muiSelectList.innerHTML.search("N-2"));
expect(muiSelectList.innerHTML.search("N-2")).toBeLessThan(muiSelectList.innerHTML.search("N-3"));
});
});

it("should select the first sorted node type in the by default", async () => {
const _id = "example-select-first-node-id";
const mocks: MockedResponse<SubmissionStatsResp>[] = [{
request: {
query: SUBMISSION_STATS,
variables: {
id: _id
},
},
result: {
data: {
submissionStats: {
stats: [
{ ...baseStatistic, nodeName: "SECOND", total: 3 },
{ ...baseStatistic, nodeName: "FIRST", total: 999 },
{ ...baseStatistic, nodeName: "THIRD", total: 1 },
],
},
},
},
}];

const { getByTestId } = render(
<TestParent mocks={mocks}>
<SubmittedDataFilters submissionId={_id} />
</TestParent>
);

const muiSelectBox = within(getByTestId("data-content-node-filter")).getByRole("button");

await waitFor(() => expect(muiSelectBox).toHaveTextContent("FIRST"));
});

// NOTE: This test no longer applies since the component fetches it's own data.
// it("should update the empty selection when the node types are populated", async () => {
// const stats: SubmissionStatistic[] = [
// { ...baseStatistic, nodeName: "FIRST-NODE", total: 999 },
// { ...baseStatistic, nodeName: "SECOND", total: 3 },
// { ...baseStatistic, nodeName: "THIRD", total: 1 },
// ];

// const { getByTestId, rerender } = render(<SubmittedDataFilters statistics={[]} />);
// const muiSelectBox = within(getByTestId("data-content-node-filter")).getByRole("button");

// rerender(<SubmittedDataFilters statistics={stats} />);

// expect(muiSelectBox).toHaveTextContent("FIRST-NODE");
// });

// NOTE: This test no longer applies since the component fetches it's own data.
// it("should not change a NON-DEFAULT selection when the node types are updated", async () => {
// const stats: SubmissionStatistic[] = [
// { ...baseStatistic, nodeName: "FIRST", total: 100 },
// { ...baseStatistic, nodeName: "SECOND", total: 2 },
// { ...baseStatistic, nodeName: "THIRD", total: 1 },
// ];

// const { getByTestId, rerender } = render(<SubmittedDataFilters statistics={stats} />);
// const muiSelectBox = within(getByTestId("data-content-node-filter")).getByRole("button");

// await waitFor(() => {
// expect(muiSelectBox).toHaveTextContent("FIRST");
// });

// // Open the dropdown
// await waitFor(() => UserEvent.click(muiSelectBox));

// // Select the 3rd option
// const firstOption = getByTestId("nodeType-THIRD");
// await waitFor(() => UserEvent.click(firstOption));

// const newStats: SubmissionStatistic[] = [
// ...stats,
// { ...baseStatistic, nodeName: "NEW-FIRST", total: 999 },
// ];

// rerender(<SubmittedDataFilters statistics={newStats} />);

// await waitFor(() => {
// // Verify the 3rd option is still selected
// expect(muiSelectBox).toHaveTextContent("THIRD");
// });
// });
});
Loading
Loading