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-2189 Compare Differences between Existing/New Data #616

Merged
merged 15 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from 11 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
Original file line number Diff line number Diff line change
Expand Up @@ -302,12 +302,12 @@ describe("ExportCrossValidationButton cases", () => {
const submissionID = "formatter-callback-sub-id";

const qcErrors = [
{ title: "Error 01", description: "Error 01 description" },
{ title: "Error 02", description: "Error 02 description" },
{ code: null, title: "Error 01", description: "Error 01 description" },
{ code: null, title: "Error 02", description: "Error 02 description" },
];
const qcWarnings = [
{ title: "Warning 01", description: "Warning 01 description" },
{ title: "Warning 02", description: "Warning 02 description" },
{ code: null, title: "Warning 01", description: "Warning 01 description" },
{ code: null, title: "Warning 02", description: "Warning 02 description" },
];

const mock: MockedResponse<CrossValidationResultsResp, CrossValidationResultsInput> = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ describe("ExportValidationButton (Expanded View) tests", () => {
submissionID: "example-dynamic-filename-id",
errors: [
{
code: null,
title: "Error 01",
description: "Error 01 description",
},
Expand Down Expand Up @@ -303,12 +304,12 @@ describe("ExportValidationButton (Expanded View) tests", () => {
const submissionID = "formatter-callback-sub-id";

const qcErrors = [
{ title: "Error 01", description: "Error 01 description" },
{ title: "Error 02", description: "Error 02 description" },
{ code: null, title: "Error 01", description: "Error 01 description" },
{ code: null, title: "Error 02", description: "Error 02 description" },
];
const qcWarnings = [
{ title: "Warning 01", description: "Warning 01 description" },
{ title: "Warning 02", description: "Warning 02 description" },
{ code: null, title: "Warning 01", description: "Warning 01 description" },
{ code: null, title: "Warning 02", description: "Warning 02 description" },
];

const mocks: MockedResponse<SubmissionQCResultsResp>[] = [
Expand Down
20 changes: 20 additions & 0 deletions src/components/ErrorDetailsDialog/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ type ParentProps = {
children: React.ReactNode;
};

jest.mock("../NodeComparison", () => ({
...jest.requireActual("../NodeComparison"),
__esModule: true,
default: () => <div>MOCK NODE COMPARISON</div>,
}));

const TestParent: FC<ParentProps> = ({ children }) => (
<MemoryRouter basename="">{children}</MemoryRouter>
);
Expand Down Expand Up @@ -106,6 +112,20 @@ describe("Basic Functionality", () => {

expect(() => userEvent.click(getByTestId("error-details-close-button"))).not.toThrow();
});

it("should render the NodeComparison component if 'comparisonData' is provided", async () => {
const { getByText } = render(
<TestParent>
<Dialog
open
errors={[]}
comparisonData={{ nodeType: "X", submissionID: "X", submittedID: "X" }}
/>
</TestParent>
);

expect(getByText("MOCK NODE COMPARISON")).toBeVisible();
});
});

describe("Implementation Requirements", () => {
Expand Down
14 changes: 12 additions & 2 deletions src/components/ErrorDetailsDialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Button, Dialog, DialogProps, IconButton, Stack, Typography, styled } fr
import React from "react";
import { ReactComponent as CloseIconSvg } from "../../assets/icons/close_icon.svg";
import { FormatDate } from "../../utils";
import NodeComparison, { NodeComparisonProps } from "../NodeComparison";

const StyledDialog = styled(Dialog)({
"& .MuiDialog-paper": {
Expand Down Expand Up @@ -109,6 +110,13 @@ type Props = {
errorCount?: string;
nodeInfo?: string;
uploadedDate?: string;
/**
* If provided, will utilize the NodeComparison component to display
* the differences between the existing and newly submitted data
*
* @see {@link NodeComparison} for more details
*/
comparisonData?: NodeComparisonProps;
onClose?: () => void;
} & Omit<DialogProps, "onClose">;

Expand All @@ -120,6 +128,7 @@ const ErrorDetailsDialog = ({
errorCount,
nodeInfo,
uploadedDate,
comparisonData,
onClose,
open,
...rest
Expand All @@ -135,7 +144,7 @@ const ErrorDetailsDialog = ({
open={open}
onClose={handleCloseDialog}
data-testid="error-details-dialog"
title=""
aria-labelledby="error-details-title"
{...rest}
>
<StyledCloseDialogButton
Expand All @@ -150,7 +159,7 @@ const ErrorDetailsDialog = ({
{header}
</StyledHeader>
)}
<StyledTitle variant="h6" data-testid="error-details-title">
<StyledTitle variant="h6" id="error-details-title" data-testid="error-details-title">
{title}
</StyledTitle>
{uploadedDate && (
Expand All @@ -177,6 +186,7 @@ const ErrorDetailsDialog = ({
))}
</StyledErrors>
</StyledErrorDetails>
{comparisonData && <NodeComparison {...comparisonData} />}
<StyledCloseButton
id="error-dialog-close-button"
data-testid="error-details-close-button"
Expand Down
225 changes: 225 additions & 0 deletions src/components/NodeComparison/ComparisonTable.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import { render } from "@testing-library/react";
import { axe } from "jest-axe";
import ComparisonTable from "./ComparisonTable";
import { RetrieveReleasedDataResp } from "../../graphql";

const baseNode: RetrieveReleasedDataResp["retrieveReleasedDataByID"][number] = {
nodeType: "",
nodeID: "",
props: "{}",
};

describe("Accessibility", () => {
it("should have no violations", async () => {
const { container, getByText } = render(
<ComparisonTable
newNode={{ ...baseNode, props: JSON.stringify({ mock_node_data_name: "bar", baz: 1 }) }}
existingNode={{
...baseNode,
props: JSON.stringify({ mock_node_data_name: "baz", baz: 2 }),
}}
loading={false}
/>
);

expect(getByText(/mock_node_data_name/i)).toBeInTheDocument();

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

it("should have no violations (loading)", async () => {
const { container } = render(<ComparisonTable newNode={null} existingNode={null} loading />);

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

it("should have no violations (no data)", async () => {
const { container } = render(
<ComparisonTable newNode={null} existingNode={null} loading={false} />
);

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

describe("Basic Functionality", () => {
it("should render without crashing", () => {
expect(() =>
render(<ComparisonTable newNode={null} existingNode={null} loading={false} />)
).not.toThrow();
});

it("should show an error message when there is no data (all null)", () => {
const { getByTestId, getByText } = render(
<ComparisonTable newNode={null} existingNode={null} loading={false} />
);

expect(getByTestId(/node-comparison-error/i)).toBeInTheDocument();
expect(getByText(/Unable to show comparison of data/i)).toBeInTheDocument();
});

it("should show an error message when there is no data (empty props)", () => {
const { getByTestId, getByText } = render(
<ComparisonTable
newNode={{ ...baseNode, props: "{}" }}
existingNode={{ ...baseNode, props: "{}" }}
loading={false}
/>
);

expect(getByTestId(/node-comparison-error/i)).toBeInTheDocument();
expect(getByText(/Unable to show comparison of data/i)).toBeInTheDocument();
});

it("should show an error message when there is no data (parsing issue 1/2)", () => {
const { getByTestId, getByText } = render(
<ComparisonTable
newNode={{ ...baseNode, props: "NOT JSON" }}
existingNode={{ ...baseNode, props: "{}" }}
loading={false}
/>
);

expect(getByTestId(/node-comparison-error/i)).toBeInTheDocument();
expect(getByText(/Unable to show comparison of data/i)).toBeInTheDocument();
});

it("should show an error message when there is no data (parsing issue 2/2)", () => {
const { getByTestId, getByText } = render(
<ComparisonTable
newNode={{ ...baseNode, props: "{}" }}
existingNode={{ ...baseNode, props: "NOT JSON" }}
loading={false}
/>
);

expect(getByTestId(/node-comparison-error/i)).toBeInTheDocument();
expect(getByText(/Unable to show comparison of data/i)).toBeInTheDocument();
});

it("should show a loading skeleton when loading", () => {
const { getAllByTestId } = render(
<ComparisonTable newNode={null} existingNode={null} loading />
);

expect(getAllByTestId("node-comparison-table-header-skeleton")).toHaveLength(5);
});

it("should rerender if a dependency changes", () => {
const { rerender, getByText, queryByText } = render(
<ComparisonTable
newNode={{
...baseNode,
props: JSON.stringify({ mock_node_data_name: "example 01", baz: 1 }),
}}
existingNode={{
...baseNode,
props: JSON.stringify({ mock_node_data_name: "example 02", baz: 2 }),
}}
loading={false}
/>
);

expect(getByText(/example 01/i)).toBeInTheDocument();
expect(getByText(/example 02/i)).toBeInTheDocument();

rerender(
<ComparisonTable
newNode={{
...baseNode,
props: JSON.stringify({ mock_node_data_name: "example 03", baz: 1 }),
}}
existingNode={{
...baseNode,
props: JSON.stringify({ mock_node_data_name: "example 04", baz: 2 }),
}}
loading={false}
/>
);

expect(queryByText(/example 01/i)).not.toBeInTheDocument();
expect(queryByText(/example 02/i)).not.toBeInTheDocument();
expect(getByText(/example 03/i)).toBeInTheDocument();
expect(getByText(/example 04/i)).toBeInTheDocument();
});
});

describe("Implementation Requirements", () => {
it("should show a table with the correct number of columns", () => {
const { getByTestId } = render(
<ComparisonTable
newNode={{
...baseNode,
props: JSON.stringify({ mock_node_data_name: "example 01", baz: 1 }),
}}
existingNode={{
...baseNode,
props: JSON.stringify({ mock_node_data_name: "example 02", baz: 2 }),
}}
loading={false}
/>
);

expect(getByTestId("node-comparison-table")).toBeInTheDocument();
expect(getByTestId("node-comparison-table").querySelectorAll("th")).toHaveLength(2);
});

it("should contain all of the header cells for each property", () => {
const dataset = {
mock_node_data_name: "example 01",
another_property: "example 02",
"property.with.dots": "yes",
};

const { getByText } = render(
<ComparisonTable
newNode={{
...baseNode,
props: JSON.stringify(dataset),
}}
existingNode={{
...baseNode,
props: JSON.stringify(dataset),
}}
loading={false}
/>
);

expect(getByText(/mock_node_data_name/i)).toBeInTheDocument();
expect(getByText(/another_property/i)).toBeInTheDocument();
expect(getByText(/property.with.dots/i)).toBeInTheDocument();
});

// NOTE: this should never happen, but covering for completeness
it("should aggregate all of the properties from both nodes", () => {
const { getByText } = render(
<ComparisonTable
newNode={{
...baseNode,
props: JSON.stringify({ mock_data_01: "example 01", another_md_02: 1 }),
}}
existingNode={{
...baseNode,
props: JSON.stringify({ mock_data_02: "example 02", another_md_03: 2 }),
}}
loading={false}
/>
);

// new
expect(getByText(/mock_data_01/i)).toBeInTheDocument();
expect(getByText(/another_md_02/i)).toBeInTheDocument();

// existing
expect(getByText(/mock_data_02/i)).toBeInTheDocument();
expect(getByText(/another_md_03/i)).toBeInTheDocument();
});
});

describe("Snapshots", () => {
it("should match the loading state snapshot", () => {
const { container } = render(<ComparisonTable newNode={null} existingNode={null} loading />);

expect(container).toMatchSnapshot();
});
});
Loading