Skip to content

Commit

Permalink
Merge pull request #317 from CBIIT/CRDCDH-881
Browse files Browse the repository at this point in the history
CRDCDH-881 Data Submission > Submitted Data tab
  • Loading branch information
Alejandro-Vega authored Apr 2, 2024
2 parents dd5468c + 967561d commit 05789f1
Show file tree
Hide file tree
Showing 14 changed files with 982 additions and 13 deletions.
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

0 comments on commit 05789f1

Please sign in to comment.