diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 68532f86e..8aa92c96a 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,4 +1,3 @@ -/* eslint-env node */ /** * @type {import("eslint").Linter.Config} */ @@ -10,7 +9,13 @@ const config = { "airbnb", "airbnb-typescript", "prettier", + "react-app", + "react-app/jest", ], + globals: { + window: true, + document: true, + }, parser: "@typescript-eslint/parser", parserOptions: { ecmaFeatures: { @@ -29,12 +34,10 @@ const config = { }, }, root: true, - ignorePatterns: ["public/js/*.js"], rules: { /* base prettier rule */ "prettier/prettier": "error", - "import/prefer-default-export": "off", "max-len": "off", "no-console": "warn", "no-param-reassign": "off", @@ -53,10 +56,7 @@ const config = { /* react rules */ "react/prop-types": "off", - "react/jsx-filename-extension": [ - 1, - { extensions: [".js", ".jsx", ".tsx", ".ts"] }, - ], + "react/jsx-filename-extension": [1, { extensions: [".js", ".jsx", ".tsx", ".ts"] }], "react/jsx-props-no-spreading": "off", "react/react-in-jsx-scope": "off", "react/require-default-props": "off", @@ -76,21 +76,17 @@ const config = { warnOnDuplicates: true, }, ], - "react/destructuring-assignment": [ - "error", - "always", - { destructureInSignature: "always" }, - ], + "react/destructuring-assignment": ["error", "always", { destructureInSignature: "always" }], /* typescript-eslint rules */ "@typescript-eslint/no-empty-function": "error", - "@typescript-eslint/no-use-before-define": ["error"], + "@typescript-eslint/no-use-before-define": "error", "@typescript-eslint/no-unused-vars": "warn", "@typescript-eslint/no-loss-of-precision": "error", "@typescript-eslint/no-redundant-type-constituents": "error", "@typescript-eslint/no-non-null-asserted-optional-chain": "error", - "@typescript-eslint/no-shadow": ["off"], - "@typescript-eslint/dot-notation": ["off"], + "@typescript-eslint/no-shadow": "off", + "@typescript-eslint/dot-notation": "off", "@typescript-eslint/naming-convention": [ "error", { @@ -99,7 +95,18 @@ const config = { leadingUnderscore: "allow", }, ], - "@typescript-eslint/ban-ts-comment": ["off"], + "@typescript-eslint/ban-ts-comment": "off", + + /* create-react-app rules */ + "react-hooks/rules-of-hooks": "off", + "react-hooks/exhaustive-deps": "off", + "import/prefer-default-export": "off", + + /* jest and testing-library rules */ + "testing-library/prefer-screen-queries": "off", + "testing-library/no-wait-for-multiple-assertions": "off", + "testing-library/no-node-access": "off", + "testing-library/no-container": "off", }, }; diff --git a/conf/inject.template.js b/conf/inject.template.js index 997b04510..5da8e2a51 100644 --- a/conf/inject.template.js +++ b/conf/inject.template.js @@ -1,11 +1,11 @@ -/* eslint-disable */ +/* eslint-disable no-template-curly-in-string */ window.injectedEnv = { - REACT_APP_BACKEND_API: '${REACT_APP_BACKEND_API}', - REACT_APP_FE_VERSION: '${REACT_APP_FE_VERSION}', - REACT_APP_GA_TRACKING_ID: '${REACT_APP_GA_TRACKING_ID}', - REACT_APP_NIH_CLIENT_ID: '${REACT_APP_NIH_CLIENT_ID}', - REACT_APP_NIH_AUTHORIZE_URL: '${NIH_AUTHORIZE_URL}', - REACT_APP_NIH_REDIRECT_URL: '${REACT_APP_NIH_REDIRECT_URL}', - REACT_APP_DEV_TIER: '${DEV_TIER}', - REACT_APP_UPLOADER_CLI: '${REACT_APP_UPLOADER_CLI}', + REACT_APP_BACKEND_API: "${REACT_APP_BACKEND_API}", + REACT_APP_FE_VERSION: "${REACT_APP_FE_VERSION}", + REACT_APP_GA_TRACKING_ID: "${REACT_APP_GA_TRACKING_ID}", + REACT_APP_NIH_CLIENT_ID: "${REACT_APP_NIH_CLIENT_ID}", + REACT_APP_NIH_AUTHORIZE_URL: "${NIH_AUTHORIZE_URL}", + REACT_APP_NIH_REDIRECT_URL: "${REACT_APP_NIH_REDIRECT_URL}", + REACT_APP_DEV_TIER: "${DEV_TIER}", + REACT_APP_UPLOADER_CLI: "${REACT_APP_UPLOADER_CLI}", }; diff --git a/package-lock.json b/package-lock.json index f538048b1..8382a3f93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "crdc-datahub-ui", - "version": "0.1.0", + "version": "3.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "crdc-datahub-ui", - "version": "0.1.0", + "version": "3.0.0", "dependencies": { "@apollo/client": "^3.9.8", "@axe-core/react": "^4.9.0", @@ -6553,7 +6553,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001495", + "version": "1.0.30001620", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001620.tgz", + "integrity": "sha512-WJvYsOjd1/BYUY6SNGUosK9DUidBPDTnOARHp3fSmFO1ekdxaY6nKRttEVrfMmYi80ctS0kz1wiWmm14fVc3ew==", "funding": [ { "type": "opencollective", @@ -6567,8 +6569,7 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/canvg": { "version": "3.0.10", diff --git a/package.json b/package.json index da7e462b9..99624b0fa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,6 @@ { "name": "crdc-datahub-ui", - "version": "0.1.0", - "private": true, + "version": "3.0.0", "dependencies": { "@apollo/client": "^3.9.8", "@axe-core/react": "^4.9.0", @@ -56,12 +55,6 @@ "typecheck": "tsc --noEmit", "prepare": "husky install" }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] - }, "browserslist": { "production": [ ">0.2%", diff --git a/public/js/injectEnv.js b/public/js/injectEnv.js index 2b41ddc82..ca3c8e8cf 100644 --- a/public/js/injectEnv.js +++ b/public/js/injectEnv.js @@ -1,10 +1,10 @@ window.injectedEnv = { - REACT_APP_NIH_AUTHORIZE_URL: '', - REACT_APP_NIH_CLIENT_ID: '', - REACT_APP_NIH_REDIRECT_URL: '', - REACT_APP_DEV_TIER: '', - REACT_APP_UPLOADER_CLI: '', - REACT_APP_GA_TRACKING_ID: '', - REACT_APP_FE_VERSION: '', - REACT_APP_BACKEND_API: '', + REACT_APP_NIH_AUTHORIZE_URL: "", + REACT_APP_NIH_CLIENT_ID: "", + REACT_APP_NIH_REDIRECT_URL: "", + REACT_APP_DEV_TIER: "", + REACT_APP_UPLOADER_CLI: "", + REACT_APP_GA_TRACKING_ID: "", + REACT_APP_FE_VERSION: "", + REACT_APP_BACKEND_API: "", }; diff --git a/public/js/session.js b/public/js/session.js index c19567c09..98c786778 100644 --- a/public/js/session.js +++ b/public/js/session.js @@ -4,31 +4,31 @@ const sessionStorageTransfer = (event) => { $event = window.event; } // ie suq if (!$event.newValue) return; // do nothing if no value to work with - if ($event.key === 'getSessionStorage') { + if ($event.key === "getSessionStorage") { // another tab asked for the sessionStorage -> send it - localStorage.setItem('sessionStorage', JSON.stringify(sessionStorage)); + localStorage.setItem("sessionStorage", JSON.stringify(sessionStorage)); // the other tab should now have it, so we're done with it. - localStorage.removeItem('sessionStorage'); // <- could do short timeout as well. - } else if ($event.key === 'sessionStorage' && !sessionStorage.length) { + localStorage.removeItem("sessionStorage"); // <- could do short timeout as well. + } else if ($event.key === "sessionStorage" && !sessionStorage.length) { // another tab sent data <- get it const data = JSON.parse($event.newValue); Object.keys(data).forEach((key) => { sessionStorage.setItem(key, data[key]); }); - } else if ($event.key === 'logout') { + } else if ($event.key === "logout") { sessionStorage.clear(); } }; // listen for changes to localStorage if (window.addEventListener) { - window.addEventListener('storage', sessionStorageTransfer, false); + window.addEventListener("storage", sessionStorageTransfer, false); } else { - window.addEventListener('onstorage', sessionStorageTransfer); + window.addEventListener("onstorage", sessionStorageTransfer); } // Ask other tabs for session storage (this is ONLY to trigger 'storage' event) if (!sessionStorage.length) { - localStorage.setItem('getSessionStorage', 'true'); - localStorage.removeItem('getSessionStorage'); + localStorage.setItem("getSessionStorage", "true"); + localStorage.removeItem("getSessionStorage"); } diff --git a/public/manifest.json b/public/manifest.json index 1f2f141fa..933117ec2 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "React App", - "name": "Create React App Sample", + "short_name": "CRDC DataHub", + "name": "CRDC DataHub", "icons": [ { "src": "favicon.ico", diff --git a/src/components/Carousel/index.tsx b/src/components/Carousel/index.tsx index 88cb9e390..e1bf65f51 100644 --- a/src/components/Carousel/index.tsx +++ b/src/components/Carousel/index.tsx @@ -1,4 +1,4 @@ -import { FC } from "react"; +import { FC, useState } from "react"; import { styled } from "@mui/material"; import Carousel, { CarouselProps } from "react-multi-carousel"; import "react-multi-carousel/lib/styles.css"; @@ -7,6 +7,10 @@ import CustomRightArrow from "./CustomRightArrow"; type Props = { children: React.ReactNode; + /** + * If true, will disable any user interaction with the carousel. + */ + locked?: boolean; } & Partial; const sizing = { @@ -16,7 +20,9 @@ const sizing = { }, }; -const StyledWrapper = styled("div")({ +const StyledWrapper = styled("div", { + shouldForwardProp: (p) => p !== "showLeftFade" && p !== "showRightFade", +})<{ showLeftFade: boolean; showRightFade: boolean }>(({ showLeftFade, showRightFade }) => ({ maxWidth: "700px", minWidth: "464px", // NOTE: Without a min-width, the carousel collapses to 0px wide width: "100%", @@ -37,7 +43,7 @@ const StyledWrapper = styled("div")({ left: "calc(100% - 162px)", top: "0", bottom: "0", - width: "162px", + width: showRightFade ? "162px" : "0px", background: "linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 50%)", zIndex: 9, }, @@ -47,11 +53,11 @@ const StyledWrapper = styled("div")({ right: "calc(100% - 162px)", top: "0", bottom: "0", - width: "162px", + width: showLeftFade ? "162px" : "0px", background: "linear-gradient(to left, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 50%)", zIndex: 9, }, -}); +})); const removeAriaHidden = () => { const elements = document.querySelectorAll(".react-multi-carousel-item"); @@ -65,22 +71,29 @@ const removeAriaHidden = () => { * @param Props * @returns {JSX.Element} */ -const ContentCarousel: FC = ({ children, ...props }: Props) => ( - - } - customRightArrow={} - {...props} - > - {children} - - -); +const ContentCarousel: FC = ({ children, locked, ...props }: Props) => { + const [activeSlide, setActiveSlide] = useState(0); + + const handleBeforeChange = (nextSlide: number) => setActiveSlide(nextSlide); + + return ( + + } + customRightArrow={} + {...props} + > + {children} + + + ); +}; export default ContentCarousel; diff --git a/src/components/Contexts/AuthContext.test.tsx b/src/components/Contexts/AuthContext.test.tsx index 2b9842687..f0694e7c0 100644 --- a/src/components/Contexts/AuthContext.test.tsx +++ b/src/components/Contexts/AuthContext.test.tsx @@ -80,15 +80,15 @@ describe("AuthContext > AuthProvider Tests", () => { localStorage.setItem("userDetails", JSON.stringify(userData)); - const screen = render(); + const { findByTestId, getByTestId } = render(); - await waitFor(() => expect(screen.getByTestId("status")).toBeInTheDocument()); + await findByTestId("status"); - expect(screen.getByTestId("status").textContent).toEqual(AuthStatus.LOADED); - expect(screen.getByTestId("isLoggedIn").textContent).toEqual("true"); - expect(screen.getByTestId("user-id").textContent).toEqual(userData._id); - expect(screen.getByTestId("first-name").textContent).toEqual(userData.firstName); - expect(screen.getByTestId("last-name").textContent).toEqual(userData.lastName); + expect(getByTestId("status").textContent).toEqual(AuthStatus.LOADED); + expect(getByTestId("isLoggedIn").textContent).toEqual("true"); + expect(getByTestId("user-id").textContent).toEqual(userData._id); + expect(getByTestId("first-name").textContent).toEqual(userData.firstName); + expect(getByTestId("last-name").textContent).toEqual(userData.lastName); }); it("should successfully verify the cached user with the BE service", async () => { @@ -115,12 +115,12 @@ describe("AuthContext > AuthProvider Tests", () => { localStorage.setItem("userDetails", JSON.stringify(userData)); - const screen = render(); + const { findByTestId, getByTestId } = render(); - await waitFor(() => expect(screen.getByTestId("status")).toBeInTheDocument()); + await findByTestId("status"); - expect(screen.getByTestId("status").textContent).toEqual(AuthStatus.LOADED); - expect(screen.getByTestId("isLoggedIn").textContent).toEqual("true"); + expect(getByTestId("status").textContent).toEqual(AuthStatus.LOADED); + expect(getByTestId("isLoggedIn").textContent).toEqual("true"); }); it("should update the localStorage cache when the user is verified", async () => { @@ -148,10 +148,10 @@ describe("AuthContext > AuthProvider Tests", () => { localStorage.setItem("userDetails", JSON.stringify(userData)); - const screen = render(); + const { getByTestId } = render(); await waitFor(() => - expect(screen.getByTestId("first-name").textContent).toEqual( + expect(getByTestId("first-name").textContent).toEqual( mocks[0].result.data.getMyUser.firstName ) ); @@ -185,13 +185,13 @@ describe("AuthContext > AuthProvider Tests", () => { localStorage.setItem("userDetails", JSON.stringify(userData)); - const screen = render(); + const { findByTestId, getByTestId } = render(); - await waitFor(() => expect(screen.getByTestId("status")).toBeInTheDocument()); + await findByTestId("status"); - expect(screen.getByTestId("status").textContent).toEqual(AuthStatus.LOADED); + expect(getByTestId("status").textContent).toEqual(AuthStatus.LOADED); - await waitFor(() => expect(screen.getByTestId("isLoggedIn").textContent).toEqual("false")); + await waitFor(() => expect(getByTestId("isLoggedIn").textContent).toEqual("false")); await waitFor( () => { diff --git a/src/components/Contexts/FormContext.test.tsx b/src/components/Contexts/FormContext.test.tsx index 7e1cb5b40..bc62a08a9 100644 --- a/src/components/Contexts/FormContext.test.tsx +++ b/src/components/Contexts/FormContext.test.tsx @@ -1,5 +1,5 @@ import React, { FC } from "react"; -import { render, waitFor } from "@testing-library/react"; +import { render } from "@testing-library/react"; import { MockedProvider, MockedResponse } from "@apollo/client/testing"; import { GraphQLError } from "graphql"; import { Status as FormStatus, FormProvider, useFormContext } from "./FormContext"; @@ -56,12 +56,12 @@ describe("FormContext > useFormContext Tests", () => { describe("FormContext > FormProvider Tests", () => { it("should return an error for empty IDs", async () => { - const screen = render(); + const { findByTestId, getByTestId } = render(); - await waitFor(() => expect(screen.getByTestId("error")).toBeInTheDocument()); + await findByTestId("error"); - expect(screen.getByTestId("status").textContent).toEqual(FormStatus.ERROR); - expect(screen.getByTestId("error").textContent).toEqual("Invalid application ID provided"); + expect(getByTestId("status").textContent).toEqual(FormStatus.ERROR); + expect(getByTestId("error").textContent).toEqual("Invalid application ID provided"); }); it("should return an error for graphql-based failures", async () => { @@ -78,16 +78,14 @@ describe("FormContext > FormProvider Tests", () => { }, }, ]; - const screen = render( + const { findByTestId, getByTestId } = render( ); - await waitFor(() => expect(screen.getByTestId("error")).toBeInTheDocument()); + await findByTestId("error"); - expect(screen.getByTestId("status").textContent).toEqual(FormStatus.ERROR); - expect(screen.getByTestId("error").textContent).toEqual( - "An unknown API or GraphQL error occurred" - ); + expect(getByTestId("status").textContent).toEqual(FormStatus.ERROR); + expect(getByTestId("error").textContent).toEqual("An unknown API or GraphQL error occurred"); }); it("should return an error for network-based failures", async () => { @@ -102,16 +100,14 @@ describe("FormContext > FormProvider Tests", () => { error: new Error("Test network error"), }, ]; - const screen = render( + const { findByTestId, getByTestId } = render( ); - await waitFor(() => expect(screen.getByTestId("error")).toBeInTheDocument()); + await findByTestId("error"); - expect(screen.getByTestId("status").textContent).toEqual(FormStatus.ERROR); - expect(screen.getByTestId("error").textContent).toEqual( - "An unknown API or GraphQL error occurred" - ); + expect(getByTestId("status").textContent).toEqual(FormStatus.ERROR); + expect(getByTestId("error").textContent).toEqual("An unknown API or GraphQL error occurred"); }); it("should return data for nominal requests", async () => { @@ -139,18 +135,16 @@ describe("FormContext > FormProvider Tests", () => { }, }, ]; - const screen = render( + const { findByTestId, getByTestId } = render( ); - await waitFor(() => expect(screen.getByTestId("status")).toBeInTheDocument()); + await findByTestId("status"); - expect(screen.getByTestId("status").textContent).toEqual(FormStatus.LOADED); - expect(screen.getByTestId("app-id").textContent).toEqual( - "556ac14a-f247-42e8-8878-8468060fb49a" - ); - expect(screen.getByTestId("pi-first-name").textContent).toEqual("Successfully"); - expect(screen.getByTestId("pi-last-name").textContent).toEqual("Fetched"); + expect(getByTestId("status").textContent).toEqual(FormStatus.LOADED); + expect(getByTestId("app-id").textContent).toEqual("556ac14a-f247-42e8-8878-8468060fb49a"); + expect(getByTestId("pi-first-name").textContent).toEqual("Successfully"); + expect(getByTestId("pi-last-name").textContent).toEqual("Fetched"); }); it("should autofill the user's last application for new submissions", async () => { @@ -174,14 +168,14 @@ describe("FormContext > FormProvider Tests", () => { }, }, ]; - const screen = render(); + const { findByTestId, getByTestId } = render(); - await waitFor(() => expect(screen.getByTestId("status")).toBeInTheDocument()); + await findByTestId("status"); - expect(screen.getByTestId("status").textContent).toEqual(FormStatus.LOADED); - expect(screen.getByTestId("app-id").textContent).toEqual("new"); - expect(screen.getByTestId("pi-first-name").textContent).toEqual("Test"); - expect(screen.getByTestId("pi-last-name").textContent).toEqual("User"); + expect(getByTestId("status").textContent).toEqual(FormStatus.LOADED); + expect(getByTestId("app-id").textContent).toEqual("new"); + expect(getByTestId("pi-first-name").textContent).toEqual("Test"); + expect(getByTestId("pi-last-name").textContent).toEqual("User"); }); it("should default to an empty string when no autofill information is returned", async () => { @@ -198,14 +192,14 @@ describe("FormContext > FormProvider Tests", () => { errors: [new GraphQLError("The user has no previous applications")], }, ]; - const screen = render(); + const { findByTestId, getByTestId } = render(); - await waitFor(() => expect(screen.getByTestId("status")).toBeInTheDocument()); + await findByTestId("status"); - expect(screen.getByTestId("status").textContent).toEqual(FormStatus.LOADED); - expect(screen.getByTestId("app-id").textContent).toEqual("new"); - expect(screen.getByTestId("pi-first-name").textContent).toEqual(""); - expect(screen.getByTestId("pi-last-name").textContent).toEqual(""); + expect(getByTestId("status").textContent).toEqual(FormStatus.LOADED); + expect(getByTestId("app-id").textContent).toEqual("new"); + expect(getByTestId("pi-first-name").textContent).toEqual(""); + expect(getByTestId("pi-last-name").textContent).toEqual(""); }); it("should autofill PI details if Section A is not started", async () => { @@ -245,13 +239,15 @@ describe("FormContext > FormProvider Tests", () => { }, }, ]; - const screen = render(); + const { findByTestId, getByTestId } = render( + + ); - await waitFor(() => expect(screen.getByTestId("status")).toBeInTheDocument()); + await findByTestId("status"); - expect(screen.getByTestId("status").textContent).toEqual(FormStatus.LOADED); - expect(screen.getByTestId("pi-first-name").textContent).toEqual("Test"); - expect(screen.getByTestId("pi-last-name").textContent).toEqual("User"); + expect(getByTestId("status").textContent).toEqual(FormStatus.LOADED); + expect(getByTestId("pi-first-name").textContent).toEqual("Test"); + expect(getByTestId("pi-last-name").textContent).toEqual("User"); }); it("should not execute getMyLastApplication if Section A is started", async () => { @@ -293,13 +289,15 @@ describe("FormContext > FormProvider Tests", () => { }, }, ]; - const screen = render(); + const { findByTestId, getByTestId } = render( + + ); - await waitFor(() => expect(screen.getByTestId("status")).toBeInTheDocument()); + await findByTestId("status"); - expect(screen.getByTestId("status").textContent).toEqual(FormStatus.LOADED); - expect(screen.getByTestId("pi-first-name").textContent).toEqual(""); - expect(screen.getByTestId("pi-last-name").textContent).toEqual(""); + expect(getByTestId("status").textContent).toEqual(FormStatus.LOADED); + expect(getByTestId("pi-first-name").textContent).toEqual(""); + expect(getByTestId("pi-last-name").textContent).toEqual(""); }); // it("should execute saveApplication when setData is called", async () => { diff --git a/src/components/DataSubmissions/CrossValidationButton.test.tsx b/src/components/DataSubmissions/CrossValidationButton.test.tsx index 9a26db5ed..68fd1b793 100644 --- a/src/components/DataSubmissions/CrossValidationButton.test.tsx +++ b/src/components/DataSubmissions/CrossValidationButton.test.tsx @@ -189,7 +189,7 @@ describe("Basic Functionality", () => { expect(called).toBe(false); - await waitFor(() => userEvent.click(getByTestId("cross-validate-button"))); + userEvent.click(getByTestId("cross-validate-button")); await waitFor(() => { expect(called).toBe(true); @@ -233,7 +233,7 @@ describe("Basic Functionality", () => { ); - await waitFor(() => userEvent.click(getByTestId("cross-validate-button"))); + userEvent.click(getByTestId("cross-validate-button")); await waitFor(() => { expect(global.mockEnqueue).toHaveBeenCalledWith("Unable to initiate validation process.", { @@ -275,7 +275,7 @@ describe("Basic Functionality", () => { ); - await waitFor(() => userEvent.click(getByTestId("cross-validate-button"))); + userEvent.click(getByTestId("cross-validate-button")); await waitFor(() => { expect(global.mockEnqueue).toHaveBeenCalledWith("Unable to initiate validation process.", { @@ -327,7 +327,7 @@ describe("Basic Functionality", () => { ); - await waitFor(() => userEvent.click(getByTestId("cross-validate-button"))); + userEvent.click(getByTestId("cross-validate-button")); await waitFor(() => { expect(onValidate).toHaveBeenCalledTimes(1); diff --git a/src/components/DataSubmissions/DataSubmissionSummary.test.tsx b/src/components/DataSubmissions/DataSubmissionSummary.test.tsx index 8ad16a4cf..9d27bbf81 100644 --- a/src/components/DataSubmissions/DataSubmissionSummary.test.tsx +++ b/src/components/DataSubmissions/DataSubmissionSummary.test.tsx @@ -1,5 +1,4 @@ import { render, fireEvent, waitFor } from "@testing-library/react"; -import { act } from "react-dom/test-utils"; import { BrowserRouter } from "react-router-dom"; import { FC, useMemo } from "react"; import { axe } from "jest-axe"; @@ -68,9 +67,7 @@ describe("DataSubmissionSummary Review Comments Dialog Tests", () => { const { getByText } = render(); - act(() => { - fireEvent.click(getByText("Review Comments")); - }); + fireEvent.click(getByText("Review Comments")); await waitFor(() => { expect(getByText(/This is the most recent review comment/)).toBeVisible(); @@ -95,9 +92,7 @@ describe("DataSubmissionSummary Review Comments Dialog Tests", () => { const { getByText } = render(); - act(() => { - fireEvent.click(getByText("Review Comments")); - }); + fireEvent.click(getByText("Review Comments")); await waitFor(() => { expect(getByText(/This is a rejected comment/)).toBeVisible(); @@ -118,14 +113,12 @@ describe("DataSubmissionSummary Review Comments Dialog Tests", () => { const { getByText, queryByText } = render(); - act(() => { - fireEvent.click(getByText("Review Comments")); - }); + fireEvent.click(getByText("Review Comments")); + await waitFor(() => expect(getByText("Comment for closing test")).toBeVisible()); - act(() => { - fireEvent.click(getByText("Close")); - }); + fireEvent.click(getByText("Close")); + await waitFor(() => expect(queryByText("Comment for closing test")).not.toBeInTheDocument()); }); @@ -144,15 +137,13 @@ describe("DataSubmissionSummary Review Comments Dialog Tests", () => { ); - act(() => { - fireEvent.click(getByText("Review Comments")); - }); + fireEvent.click(getByText("Review Comments")); + await waitFor(() => expect(getByText("Another comment for close icon test")).toBeVisible()); const closeButton = getByTestId("review-comments-dialog-close-icon-button"); - act(() => { - fireEvent.click(closeButton); - }); + fireEvent.click(closeButton); + await waitFor(() => expect(queryByText("Another comment for close icon test")).not.toBeInTheDocument() ); @@ -179,9 +170,7 @@ describe("DataSubmissionSummary History Dialog Tests", () => { const { getByText } = render(); - act(() => { - fireEvent.click(getByText("Full History")); - }); + fireEvent.click(getByText("Full History")); await waitFor(() => { expect(getByText("SUBMITTED")).toBeVisible(); @@ -206,9 +195,7 @@ describe("DataSubmissionSummary History Dialog Tests", () => { const { getAllByTestId, getByText } = render(); - act(() => { - fireEvent.click(getByText("Full History")); - }); + fireEvent.click(getByText("Full History")); const elements = getAllByTestId("history-item"); expect(elements[0]).toHaveTextContent(/ARCHIVED/i); @@ -228,14 +215,12 @@ describe("DataSubmissionSummary History Dialog Tests", () => { const { getByText, queryByTestId } = render(); - act(() => { - fireEvent.click(getByText("Full History")); - }); + fireEvent.click(getByText("Full History")); + await waitFor(() => expect(queryByTestId("history-dialog")).toBeVisible()); - act(() => { - fireEvent.click(queryByTestId("history-dialog-close")); - }); + fireEvent.click(queryByTestId("history-dialog-close")); + await waitFor(() => expect(queryByTestId("history-dialog")).not.toBeInTheDocument()); }); @@ -250,9 +235,7 @@ describe("DataSubmissionSummary History Dialog Tests", () => { const { getByText, getAllByTestId } = render(); - act(() => { - fireEvent.click(getByText("Full History")); - }); + fireEvent.click(getByText("Full History")); await waitFor(() => { const items = getAllByTestId("history-item-date"); @@ -275,9 +258,7 @@ describe("DataSubmissionSummary History Dialog Tests", () => { const { getByTestId, getByText } = render(); - act(() => { - fireEvent.click(getByText("Full History")); - }); + fireEvent.click(getByText("Full History")); expect(getByTestId("history-item-0-icon")).toBeVisible(); expect(() => getByTestId("history-item-1-icon")).toThrow(); @@ -292,9 +273,7 @@ describe("DataSubmissionSummary History Dialog Tests", () => { const { getByTestId, getByText } = render(); - act(() => { - fireEvent.click(getByText("Full History")); - }); + fireEvent.click(getByText("Full History")); const icon = getByTestId("history-item-0-icon"); diff --git a/src/components/DataSubmissions/DataUpload.test.tsx b/src/components/DataSubmissions/DataUpload.test.tsx index 3da167bcc..ebb40a7c9 100644 --- a/src/components/DataSubmissions/DataUpload.test.tsx +++ b/src/components/DataSubmissions/DataUpload.test.tsx @@ -109,9 +109,7 @@ describe("Basic Functionality", () => { userEvent.click(getByTestId("uploader-cli-config-button")); // Skip filling the fields and click the download button - await waitFor(() => { - userEvent.click(getByText("Download")); - }); + userEvent.click(getByText("Download")); await waitFor(() => { expect(global.mockEnqueue).toHaveBeenCalledWith( @@ -143,12 +141,10 @@ describe("Basic Functionality", () => { ); // Open the dialog - await waitFor(() => userEvent.click(getByTestId("uploader-cli-config-button"))); + userEvent.click(getByTestId("uploader-cli-config-button")); // Skip filling the fields and click the download button - await waitFor(() => { - userEvent.click(getByText("Download")); - }); + userEvent.click(getByText("Download")); await waitFor(() => { expect(global.mockEnqueue).toHaveBeenCalledWith( @@ -174,7 +170,7 @@ describe("Implementation Requirements", () => { expect(getByText(/CLI Tool download/i)).toBeVisible(); expect(link).toContainElement(getByText(/CLI Tool download/i)); - await act(async () => userEvent.click(link)); + userEvent.click(link); await waitFor(() => { expect(getByText(/Uploader CLI Tool/i)).toBeInTheDocument(); @@ -224,11 +220,10 @@ describe("Implementation Requirements", () => { expect(called).toBe(false); // Open the dialog - await act(async () => { - userEvent.click(getByTestId("uploader-cli-config-button")); - }); + userEvent.click(getByTestId("uploader-cli-config-button")); // Skip filling the fields and click the download button + // eslint-disable-next-line testing-library/no-unnecessary-act -- RHF is throwing an error without act await act(async () => { userEvent.click(getByText("Download")); }); @@ -271,12 +266,10 @@ describe("Implementation Requirements", () => { ); // Open the dialog - await act(async () => userEvent.click(getByTestId("uploader-cli-config-button"))); + userEvent.click(getByTestId("uploader-cli-config-button")); // Skip filling the fields and click the download button - await act(async () => { - userEvent.click(getByText("Download")); - }); + userEvent.click(getByText("Download")); await waitFor(() => { expect(mockDownloadBlob).toHaveBeenCalledWith( diff --git a/src/components/DataSubmissions/DeleteAllOrphanFilesButton.test.tsx b/src/components/DataSubmissions/DeleteAllOrphanFilesButton.test.tsx index 096c180b1..a972202e8 100644 --- a/src/components/DataSubmissions/DeleteAllOrphanFilesButton.test.tsx +++ b/src/components/DataSubmissions/DeleteAllOrphanFilesButton.test.tsx @@ -254,7 +254,7 @@ describe("DeleteAllOrphanFilesButton Component", () => { }); }); - it("should call onDelete with false and show error message on failed mutation", async () => { + it("should call onDelete with false and show error message on failed mutation (network failure)", async () => { const { getByTestId } = render( { }); }); - it("should call onDelete with false and show error message on failed mutation", async () => { + it("should call onDelete with false and show error message on failed mutation (GraphQL error)", async () => { const { getByTestId } = render( { }); }); - it("should call onDelete with false and show error message on failed mutation", async () => { + it("should call onDelete with false and show error message on failed mutation (API failure)", async () => { const { getByTestId } = render( { }); }); - it("should call onDeleteFile with false and show error message on failed mutation", async () => { - const { getByTestId } = render( - - - - ); - - userEvent.click(getByTestId("delete-orphaned-file-chip")); - - await waitFor(() => { - expect(onDeleteFile).toHaveBeenCalledWith(false); - expect(global.mockEnqueue).toHaveBeenCalledWith( - "There was an issue deleting orphaned file.", - { - variant: "error", - } - ); - }); - }); - it("should call onDeleteFile with false and show error message on graphql error", async () => { const { getByTestId } = render( { expect(called).toBe(false); // NOTE: This must be separate from the expect below to ensure its not called multiple times - await waitFor(() => UserEvent.click(getByTestId("export-validation-button"))); + UserEvent.click(getByTestId("export-validation-button")); await waitFor(() => { expect(called).toBe(true); }); @@ -192,9 +192,7 @@ describe("ExportValidationButton cases", () => { ); - act(() => { - fireEvent.click(getByTestId("export-validation-button")); - }); + fireEvent.click(getByTestId("export-validation-button")); await waitFor(() => { const filename = `${expected}-2021-01-19T145401.csv`; @@ -242,9 +240,7 @@ describe("ExportValidationButton cases", () => { ); - act(() => { - fireEvent.click(getByTestId("export-validation-button")); - }); + fireEvent.click(getByTestId("export-validation-button")); await waitFor(() => { expect(global.mockEnqueue).toHaveBeenCalledWith( @@ -329,9 +325,7 @@ describe("ExportValidationButton cases", () => { ); - act(() => { - fireEvent.click(getByTestId("export-validation-button")); - }); + fireEvent.click(getByTestId("export-validation-button")); await waitFor(() => { // NOTE: The results are unpacked, 3 QCResults with 2 errors and 2 warnings each = 12 calls @@ -365,9 +359,7 @@ describe("ExportValidationButton cases", () => { ); - act(() => { - fireEvent.click(getByTestId("export-validation-button")); - }); + fireEvent.click(getByTestId("export-validation-button")); await waitFor(() => { expect(global.mockEnqueue).toHaveBeenCalledWith( @@ -406,9 +398,7 @@ describe("ExportValidationButton cases", () => { ); - act(() => { - fireEvent.click(getByTestId("export-validation-button")); - }); + fireEvent.click(getByTestId("export-validation-button")); await waitFor(() => { expect(global.mockEnqueue).toHaveBeenCalledWith( @@ -456,9 +446,7 @@ describe("ExportValidationButton cases", () => { ); - act(() => { - fireEvent.click(getByTestId("export-validation-button")); - }); + fireEvent.click(getByTestId("export-validation-button")); await waitFor(() => { expect(global.mockEnqueue).toHaveBeenCalledWith("Unable to export validation results.", { diff --git a/src/components/DataSubmissions/MetadataUpload.test.tsx b/src/components/DataSubmissions/MetadataUpload.test.tsx index db9ddbbdc..ccbd0878e 100644 --- a/src/components/DataSubmissions/MetadataUpload.test.tsx +++ b/src/components/DataSubmissions/MetadataUpload.test.tsx @@ -1,5 +1,5 @@ import { FC } from "react"; -import { act, getByLabelText, render, waitFor } from "@testing-library/react"; +import { getByLabelText, render, waitFor } from "@testing-library/react"; import { MockedProvider, MockedResponse } from "@apollo/client/testing"; import { axe } from "jest-axe"; import userEvent from "@testing-library/user-event"; @@ -256,7 +256,7 @@ describe("Basic Functionality", () => { const file = new File(["unused-content"], "metadata.txt", { type: "text/plain" }); userEvent.upload(getByTestId("metadata-upload-file-input"), file); - await act(async () => userEvent.click(getByTestId("metadata-upload-file-upload-button"))); + userEvent.click(getByTestId("metadata-upload-file-upload-button")); await waitFor(() => { expect(onCreateBatchMock).toHaveBeenCalledTimes(1); @@ -319,7 +319,7 @@ describe("Basic Functionality", () => { const file = new File(["unused-content"], "metadata.txt", { type: "text/plain" }); userEvent.upload(getByTestId("metadata-upload-file-input"), file); - await act(async () => userEvent.click(getByTestId("metadata-upload-file-upload-button"))); + userEvent.click(getByTestId("metadata-upload-file-upload-button")); await waitFor(() => { expect(onUploadMock).toHaveBeenCalledWith(expect.any(String), "success"); @@ -377,7 +377,7 @@ describe("Basic Functionality", () => { const file = new File(["unused-content"], "metadata.txt", { type: "text/plain" }); userEvent.upload(getByTestId("metadata-upload-file-input"), file); - await act(async () => userEvent.click(getByTestId("metadata-upload-file-upload-button"))); + userEvent.click(getByTestId("metadata-upload-file-upload-button")); await waitFor(() => { expect(onUploadMock).toHaveBeenCalledWith(expect.any(String), "error"); @@ -436,7 +436,7 @@ describe("Basic Functionality", () => { const file = new File(["unused-content"], "metadata.txt", { type: "text/plain" }); userEvent.upload(getByTestId("metadata-upload-file-input"), file); - await act(async () => userEvent.click(getByTestId("metadata-upload-file-upload-button"))); + userEvent.click(getByTestId("metadata-upload-file-upload-button")); await waitFor(() => { expect(onUploadMock).toHaveBeenCalledWith(expect.any(String), "error"); @@ -503,7 +503,7 @@ describe("Basic Functionality", () => { const file = new File(["unused-content"], "metadata.txt", { type: "text/plain" }); userEvent.upload(getByTestId("metadata-upload-file-input"), file); - await act(async () => userEvent.click(getByTestId("metadata-upload-file-upload-button"))); + userEvent.click(getByTestId("metadata-upload-file-upload-button")); await waitFor(() => { expect(onUploadMock).toHaveBeenCalledWith(expect.any(String), "error"); @@ -580,7 +580,7 @@ describe("Implementation Requirements", () => { userEvent.upload(getByTestId("metadata-upload-file-input"), file); // NOTE: We're not awaiting this because we want to test the button state before the upload is complete - act(() => userEvent.click(getByTestId("metadata-upload-file-upload-button"))); + userEvent.click(getByTestId("metadata-upload-file-upload-button")); await waitFor(() => expect(getByTestId("metadata-upload-file-upload-button")).toHaveTextContent(/Uploading.../i) @@ -932,6 +932,7 @@ describe("Implementation Requirements", () => { const updateRadio = getByLabelText(radio, "Add/Change"); const deleteRadio = getByLabelText(radio, "Remove"); + /* eslint-disable jest/no-conditional-expect */ if (expected === false) { expect(newRadio).toBeDisabled(); expect(updateRadio).toBeDisabled(); @@ -941,6 +942,7 @@ describe("Implementation Requirements", () => { expect(updateRadio).toBeEnabled(); expect(deleteRadio).toBeEnabled(); } + /* eslint-enable jest/no-conditional-expect */ } ); diff --git a/src/components/DataSubmissions/MetadataUpload.tsx b/src/components/DataSubmissions/MetadataUpload.tsx index cd2ca7230..6fd5ab5bb 100644 --- a/src/components/DataSubmissions/MetadataUpload.tsx +++ b/src/components/DataSubmissions/MetadataUpload.tsx @@ -250,9 +250,7 @@ export const MetadataUpload = ({ submission, readOnly, onCreateBatch, onUpload } } // Batch upload completed successfully onUpload( - `${selectedFiles.length} ${selectedFiles.length > 1 ? "Files" : "File"} successfully ${ - metadataIntention === "Remove" ? "removed" : "uploaded" - }`, + "The batch upload is in progress. You can check the upload status in the Data Activity tab once the upload is complete", "success" ); setIsUploading(false); diff --git a/src/components/DataSubmissions/SubmittedDataFilters.test.tsx b/src/components/DataSubmissions/SubmittedDataFilters.test.tsx index 508c6cf2f..f37bbeadf 100644 --- a/src/components/DataSubmissions/SubmittedDataFilters.test.tsx +++ b/src/components/DataSubmissions/SubmittedDataFilters.test.tsx @@ -3,7 +3,6 @@ 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 { act } from "react-dom/test-utils"; import { SubmittedDataFilters } from "./SubmittedDataFilters"; import { SUBMISSION_STATS, SubmissionStatsResp } from "../../graphql"; @@ -98,9 +97,9 @@ describe("SubmittedDataFilters cases", () => { const muiSelectBox = within(getByTestId("data-content-node-filter")).getByRole("button"); - await waitFor(() => { - UserEvent.click(muiSelectBox); + UserEvent.click(muiSelectBox); + await waitFor(() => { const muiSelectList = within(getByTestId("data-content-node-filter")).getByRole("listbox", { hidden: true, }); @@ -180,7 +179,7 @@ describe("SubmittedDataFilters cases", () => { const muiSelectBox = within(getByTestId("data-content-node-filter")).getByRole("button"); - await act(async () => UserEvent.click(muiSelectBox)); + UserEvent.click(muiSelectBox); await waitFor(() => { // Sanity check that the box is open diff --git a/src/components/DataSubmissions/ValidationControls.test.tsx b/src/components/DataSubmissions/ValidationControls.test.tsx index 2afbf3cb3..97a5f96d9 100644 --- a/src/components/DataSubmissions/ValidationControls.test.tsx +++ b/src/components/DataSubmissions/ValidationControls.test.tsx @@ -1,5 +1,5 @@ import { FC } from "react"; -import { act, getByLabelText, render, waitFor } from "@testing-library/react"; +import { getByLabelText, render, waitFor } from "@testing-library/react"; import { MockedProvider, MockedResponse } from "@apollo/client/testing"; import { axe } from "jest-axe"; import userEvent from "@testing-library/user-event"; @@ -224,7 +224,7 @@ describe("Basic Functionality", () => { const radio = getByTestId("validate-controls-validation-type") as HTMLInputElement; - await act(async () => userEvent.click(getByLabelText(radio, "Validate Metadata"))); + userEvent.click(getByLabelText(radio, "Validate Metadata")); userEvent.click(getByTestId("validate-controls-validate-button")); @@ -278,7 +278,7 @@ describe("Basic Functionality", () => { expect(called).toBe(false); const radio = getByTestId("validate-controls-validation-type") as HTMLInputElement; - await act(async () => userEvent.click(getByLabelText(radio, "Validate Data Files"))); + userEvent.click(getByLabelText(radio, "Validate Data Files")); userEvent.click(getByTestId("validate-controls-validate-button")); @@ -332,7 +332,7 @@ describe("Basic Functionality", () => { expect(called).toBe(false); const radio = getByTestId("validate-controls-validation-type") as HTMLInputElement; - await act(async () => userEvent.click(getByLabelText(radio, "Both"))); + userEvent.click(getByLabelText(radio, "Both")); userEvent.click(getByTestId("validate-controls-validate-button")); @@ -386,7 +386,7 @@ describe("Basic Functionality", () => { expect(called).toBe(false); const radio = getByTestId("validate-controls-validation-target") as HTMLInputElement; - await act(async () => userEvent.click(getByLabelText(radio, "New Uploaded Data"))); + userEvent.click(getByLabelText(radio, "New Uploaded Data")); userEvent.click(getByTestId("validate-controls-validate-button")); @@ -445,7 +445,7 @@ describe("Basic Functionality", () => { expect(called).toBe(false); const radio = getByTestId("validate-controls-validation-target") as HTMLInputElement; - await act(async () => userEvent.click(getByLabelText(radio, `${target} Uploaded Data`))); + userEvent.click(getByLabelText(radio, `${target} Uploaded Data`)); userEvent.click(getByTestId("validate-controls-validate-button")); @@ -482,7 +482,7 @@ describe("Basic Functionality", () => { ); - await waitFor(() => userEvent.click(getByTestId("validate-controls-validate-button"))); + userEvent.click(getByTestId("validate-controls-validate-button")); await waitFor(() => { expect(global.mockEnqueue).toHaveBeenCalledWith("Unable to initiate validation process.", { @@ -521,7 +521,7 @@ describe("Basic Functionality", () => { ); - await waitFor(() => userEvent.click(getByTestId("validate-controls-validate-button"))); + userEvent.click(getByTestId("validate-controls-validate-button")); await waitFor(() => { expect(global.mockEnqueue).toHaveBeenCalledWith("Unable to initiate validation process.", { @@ -570,7 +570,7 @@ describe("Basic Functionality", () => { ); - await waitFor(() => userEvent.click(getByTestId("validate-controls-validate-button"))); + userEvent.click(getByTestId("validate-controls-validate-button")); await waitFor(() => { expect(onValidate).toHaveBeenCalledTimes(1); @@ -660,11 +660,11 @@ describe("Implementation Requirements", () => { // Change from default type const typeRadio = getByTestId("validate-controls-validation-type") as HTMLInputElement; - await act(async () => userEvent.click(getByLabelText(typeRadio, "Both"))); + userEvent.click(getByLabelText(typeRadio, "Both")); // Change from default target const targetRadio = getByTestId("validate-controls-validation-target") as HTMLInputElement; - await act(async () => userEvent.click(getByLabelText(targetRadio, `All Uploaded Data`))); + userEvent.click(getByLabelText(targetRadio, `All Uploaded Data`)); userEvent.click(getByTestId("validate-controls-validate-button")); diff --git a/src/components/DataSubmissions/ValidationStatistics.tsx b/src/components/DataSubmissions/ValidationStatistics.tsx index bdebc7042..7c12c0932 100644 --- a/src/components/DataSubmissions/ValidationStatistics.tsx +++ b/src/components/DataSubmissions/ValidationStatistics.tsx @@ -1,6 +1,6 @@ import React, { FC, useMemo, useState } from "react"; import { cloneDeep, isEqual } from "lodash"; -import { Box, Stack, StackProps, Tab, Tabs, Typography, styled } from "@mui/material"; +import { Stack, StackProps, Tab, Tabs, Typography, styled } from "@mui/material"; import ContentCarousel from "../Carousel"; import NodeTotalChart from "../NodeTotalChart"; import MiniPieChart from "../NodeChart"; @@ -98,10 +98,6 @@ const StyledTab = styled(Tab)({ }, }); -const PaddingBox = styled(Box)({ - width: "175px", -}); - const defaultFilters: LegendFilter[] = [ { label: "New", color: "#4D90D3", disabled: false }, { label: "Passed", color: "#32E69A", disabled: false }, @@ -177,8 +173,12 @@ const DataSubmissionStatistics: FC = ({ dataSubmission, statistics }: Pro Individual Node Types {`(${dataset.length})`} - - {dataset.length > 2 && } + {/* NOTE: The transform is derived from the difference of Chart width and + chart container width which is 50px on each side (100px) */} + 3 ? 100 : 0} + locked={dataset.length <= 3} + > {dataset?.map((stat) => ( = ({ dataSubmission, statistics }: Pro data={buildMiniChartSeries(stat, disabledSeries)} /> ))} + {/* NOTE: the 4th node is cut-off without this */} + {dataset.length === 4 && } diff --git a/src/components/ProgressBar/ProgressBar.test.tsx b/src/components/ProgressBar/ProgressBar.test.tsx index 0fcf63e0e..bd4762f56 100644 --- a/src/components/ProgressBar/ProgressBar.test.tsx +++ b/src/components/ProgressBar/ProgressBar.test.tsx @@ -102,12 +102,12 @@ describe("ProgressBar General Tests", () => { const sections = Object.values(config); it("renders the progress bar with all A-D config-defined sections", () => { - const screen = render(); + const { getByText } = render(); sections .filter((section) => section.id !== config.REVIEW.id) .forEach(({ title }, index) => { - const root = screen.getByText(title).closest("a"); + const root = getByText(title).closest("a"); expect(root).toBeVisible(); expect(root).toHaveAttribute("data-testId", `progress-bar-section-${index}`); diff --git a/src/components/Shared/ReviewCommentsDialog.test.tsx b/src/components/Shared/ReviewCommentsDialog.test.tsx index 529b63fad..c0dedc90c 100644 --- a/src/components/Shared/ReviewCommentsDialog.test.tsx +++ b/src/components/Shared/ReviewCommentsDialog.test.tsx @@ -1,7 +1,6 @@ import { ThemeProvider, rgbToHex } from "@mui/material"; import { BrowserRouter } from "react-router-dom"; import { render, fireEvent, waitFor } from "@testing-library/react"; -import { act } from "react-dom/test-utils"; import { axe } from "jest-axe"; import ReviewCommentsDialog from "./ReviewCommentsDialog"; import theme from "../../theme"; @@ -106,9 +105,7 @@ describe("ReviewCommentsDialog Tests", () => { const { getByTestId } = render(); - act(() => { - fireEvent.click(getByTestId("review-comments-dialog-close")); - }); + fireEvent.click(getByTestId("review-comments-dialog-close")); await waitFor(() => expect(mockClose).toHaveBeenCalled()); }); diff --git a/src/components/StatusBar/StatusBar.test.tsx b/src/components/StatusBar/StatusBar.test.tsx index b503df230..0652b7328 100644 --- a/src/components/StatusBar/StatusBar.test.tsx +++ b/src/components/StatusBar/StatusBar.test.tsx @@ -1,7 +1,6 @@ import { FC, useMemo } from "react"; import { BrowserRouter } from "react-router-dom"; import { fireEvent, render, waitFor } from "@testing-library/react"; -import { act } from "react-dom/test-utils"; import { axe } from "jest-axe"; import { ContextState, Context as FormCtx, Status as FormStatus } from "../Contexts/FormContext"; import StatusBar from "./StatusBar"; @@ -214,9 +213,7 @@ describe("StatusBar > Comments Modal Tests", () => { const { getByTestId, getByText } = render(); - act(() => { - fireEvent.click(getByText("Review Comments")); - }); + fireEvent.click(getByText("Review Comments")); expect(getByTestId("review-comments-dialog")).toBeVisible(); }); @@ -235,9 +232,7 @@ describe("StatusBar > Comments Modal Tests", () => { const { getByTestId, getByText } = render(); - act(() => { - fireEvent.click(getByText("Review Comments")); - }); + fireEvent.click(getByText("Review Comments")); expect(getByTestId("review-comments-dialog")).toBeVisible(); expect(getByText(/BASED ON SUBMISSION FROM 11\/30\/2019:/i)).toBeVisible(); @@ -259,9 +254,7 @@ describe("StatusBar > Comments Modal Tests", () => { const { getByTestId, getByText } = render(); - act(() => { - fireEvent.click(getByText("Review Comments")); - }); + fireEvent.click(getByText("Review Comments")); expect(getByTestId("review-comments-dialog")).toBeVisible(); expect(getByText(/BASED ON SUBMISSION FROM 12\/30\/2023:/i)).toBeVisible(); @@ -283,9 +276,7 @@ describe("StatusBar > Comments Modal Tests", () => { const { getByTestId, getByText } = render(); - act(() => { - fireEvent.click(getByText("Review Comments")); - }); + fireEvent.click(getByText("Review Comments")); expect(getByTestId("review-comments-dialog")).toBeVisible(); expect(getByText(/BASED ON SUBMISSION FROM 11\/26\/2023:/i)).toBeVisible(); @@ -299,9 +290,7 @@ describe("StatusBar > Comments Modal Tests", () => { const { getByText } = render(); - act(() => { - fireEvent.click(getByText("Review Comments")); - }); + fireEvent.click(getByText("Review Comments")); expect(getByText(/BASED ON SUBMISSION FROM 11\/24\/2009:/i)).toHaveAttribute( "title", @@ -316,15 +305,11 @@ describe("StatusBar > Comments Modal Tests", () => { const { queryByTestId, getByText } = render(); - act(() => { - fireEvent.click(getByText("Review Comments")); - }); + fireEvent.click(getByText("Review Comments")); expect(queryByTestId("review-comments-dialog")).toBeVisible(); - act(() => { - fireEvent.click(queryByTestId("review-comments-dialog-close")); - }); + fireEvent.click(queryByTestId("review-comments-dialog-close")); await waitFor(() => expect(queryByTestId("review-comments-dialog")).toBeNull()); }); @@ -348,9 +333,7 @@ describe("StatusBar > History Modal Tests", () => { const { getByTestId, getByText } = render(); - act(() => { - fireEvent.click(getByText("Full History")); - }); + fireEvent.click(getByText("Full History")); expect(getByTestId("status-bar-history-dialog")).toBeVisible(); }); @@ -366,9 +349,7 @@ describe("StatusBar > History Modal Tests", () => { const { getByTestId, getByText } = render(); - act(() => { - fireEvent.click(getByText("Full History")); - }); + fireEvent.click(getByText("Full History")); const elements = getByTestId("status-bar-history-dialog").querySelectorAll("li"); expect(elements[0]).toHaveTextContent(/Rejected/i); @@ -389,9 +370,7 @@ describe("StatusBar > History Modal Tests", () => { const { getByTestId, getByText } = render(); - act(() => { - fireEvent.click(getByText("Full History")); - }); + fireEvent.click(getByText("Full History")); expect(getByTestId("status-bar-history-item-0-icon")).toBeVisible(); expect(() => getByTestId("status-bar-history-item-1-icon")).toThrow(); @@ -406,9 +385,7 @@ describe("StatusBar > History Modal Tests", () => { const { getByTestId, getByText } = render(); - act(() => { - fireEvent.click(getByText("Full History")); - }); + fireEvent.click(getByText("Full History")); const icon = getByTestId("status-bar-history-item-0-icon"); @@ -425,9 +402,7 @@ describe("StatusBar > History Modal Tests", () => { const { getByText } = render(); - act(() => { - fireEvent.click(getByText("Full History")); - }); + fireEvent.click(getByText("Full History")); expect(getByText("11/24/2009")).toHaveAttribute("title", data.history[0].dateTime); }); @@ -439,15 +414,11 @@ describe("StatusBar > History Modal Tests", () => { const { queryByTestId, getByText } = render(); - act(() => { - fireEvent.click(getByText("Full History")); - }); + fireEvent.click(getByText("Full History")); expect(queryByTestId("status-bar-history-dialog")).toBeVisible(); - act(() => { - fireEvent.click(queryByTestId("status-bar-dialog-close")); - }); + fireEvent.click(queryByTestId("status-bar-dialog-close")); await waitFor(() => expect(queryByTestId("status-bar-history-dialog")).toBeNull()); }); diff --git a/src/components/SystemUseWarningOverlay/OverlayText.tsx b/src/components/SystemUseWarningOverlay/OverlayText.tsx index c06419e28..b674ad785 100644 --- a/src/components/SystemUseWarningOverlay/OverlayText.tsx +++ b/src/components/SystemUseWarningOverlay/OverlayText.tsx @@ -1,4 +1,4 @@ -export default { +const OverlayContent = { content: [ "This warning banner provides privacy and security notices consistent with applicable federal laws, directives, and other federal guidance for accessing this Government system, which includes (1) this computer network, (2) all computers connected to this network, and (3) all devices and storage media attached to this network or to a computer on this network.", "This system is provided for Government-authorized use only.", @@ -11,3 +11,5 @@ export default { "Any communication or data transiting or stored on this system may be disclosed or used for any lawful Government purpose.", ], }; + +export default OverlayContent; diff --git a/src/components/SystemUseWarningOverlay/OverlayWindow.tsx b/src/components/SystemUseWarningOverlay/OverlayWindow.tsx index 0b69012fc..7f67b548e 100644 --- a/src/components/SystemUseWarningOverlay/OverlayWindow.tsx +++ b/src/components/SystemUseWarningOverlay/OverlayWindow.tsx @@ -184,6 +184,7 @@ const OverlayWindow = () => { open={open} aria-labelledby="alert-dialog-title" aria-describedby="alert-dialog-description" + data-testid="system-use-warning-dialog" maxWidth="md" > Warning diff --git a/src/components/SystemUseWarningOverlay/index.test.tsx b/src/components/SystemUseWarningOverlay/index.test.tsx index 3c3e9b54c..5525f2e94 100644 --- a/src/components/SystemUseWarningOverlay/index.test.tsx +++ b/src/components/SystemUseWarningOverlay/index.test.tsx @@ -1,5 +1,5 @@ import { axe } from "jest-axe"; -import { render, waitFor } from "@testing-library/react"; +import { render } from "@testing-library/react"; import OverlayWindow from "./OverlayWindow"; beforeEach(() => { @@ -7,11 +7,9 @@ beforeEach(() => { }); it("should not have any accessibility violations", async () => { - const { container } = render(); + const { container, findByTestId } = render(); - await waitFor(() => container.querySelector("#alert-dialog-title")); + await findByTestId("system-use-warning-dialog"); - const results = await axe(container); - - expect(results).toHaveNoViolations(); + expect(await axe(container)).toHaveNoViolations(); }); diff --git a/src/config/globalFooterData.tsx b/src/config/globalFooterData.tsx index 15c072042..a9ea7fcfd 100644 --- a/src/config/globalFooterData.tsx +++ b/src/config/globalFooterData.tsx @@ -5,7 +5,7 @@ import youtubeIcon from "../assets/footer/Youtube_Logo.svg"; import linkedInIcon from "../assets/footer/LinkedIn_Logo.svg"; // footerLogoImage ideal image size 310x80 px -export default { +const FooterConfig = { footerLogoImage: "https://raw.githubusercontent.com/cbiit/datacommons-assets/main/bento/images/icons/png/footerlogo.png", footerLogoAltText: "Footer Logo", @@ -127,3 +127,5 @@ export default { }, ], }; + +export default FooterConfig; diff --git a/src/content/dataSubmissions/Controller.tsx b/src/content/dataSubmissions/Controller.tsx index 054a3d0ef..275cf98fb 100644 --- a/src/content/dataSubmissions/Controller.tsx +++ b/src/content/dataSubmissions/Controller.tsx @@ -9,7 +9,7 @@ import ListView from "./DataSubmissionsListView"; * @param {void} * @returns {FC} - React component */ -export default () => { +const DataSubmissionController = () => { const { submissionId, tab } = useParams(); if (submissionId) { @@ -18,3 +18,5 @@ export default () => { return ; }; + +export default DataSubmissionController; diff --git a/src/content/dataSubmissions/ErrorDialog.tsx b/src/content/dataSubmissions/ErrorDialog.tsx index cde199831..1b09c0353 100644 --- a/src/content/dataSubmissions/ErrorDialog.tsx +++ b/src/content/dataSubmissions/ErrorDialog.tsx @@ -107,6 +107,7 @@ type Props = { closeText?: string; errors: string[]; errorCount?: string; + nodeInfo?: string; uploadedDate?: string; onClose?: () => void; } & Omit; @@ -117,6 +118,7 @@ const ErrorDialog = ({ closeText = "Close", errors, errorCount, + nodeInfo, uploadedDate, onClose, open, @@ -140,6 +142,7 @@ const ErrorDialog = ({ Uploaded on {FormatDate(uploadedDate, "M/D/YYYY", "N/A")} )} + {nodeInfo && {nodeInfo}} {errorCount || `${errors?.length || 0} ${errors?.length === 1 ? "ERROR" : "ERRORS"}`} diff --git a/src/content/dataSubmissions/QualityControl.tsx b/src/content/dataSubmissions/QualityControl.tsx index c71b2e687..3b8455367 100644 --- a/src/content/dataSubmissions/QualityControl.tsx +++ b/src/content/dataSubmissions/QualityControl.tsx @@ -14,7 +14,7 @@ import { SubmissionQCResultsResp, } from "../../graphql"; import GenericTable, { Column } from "../../components/DataSubmissions/GenericTable"; -import { FormatDate, capitalizeFirstLetter } from "../../utils"; +import { FormatDate, titleCase } from "../../utils"; import ErrorDialog from "./ErrorDialog"; import QCResultsContext from "./Contexts/QCResultsContext"; import { ExportValidationButton } from "../../components/DataSubmissions/ExportValidationButton"; @@ -423,9 +423,10 @@ const QualityControl: FC = ({ submission, refreshSubmission }: Props) => open={openErrorDialog} onClose={() => setOpenErrorDialog(false)} header={null} - title={`Validation Issues for ${capitalizeFirstLetter( - selectedRow?.type - )} Node ID ${selectedRow?.submittedID}.`} + title="Validation Issues" + nodeInfo={`For ${titleCase(selectedRow?.type)}${ + selectedRow?.type?.toLocaleLowerCase() !== "data file" ? " Node" : "" + } ID ${selectedRow?.submittedID}`} errors={allDescriptions} errorCount={`${allDescriptions?.length || 0} ${ allDescriptions?.length === 1 ? "ISSUE" : "ISSUES" diff --git a/src/content/organizations/Controller.tsx b/src/content/organizations/Controller.tsx index 4deaab5d5..1012d41d1 100644 --- a/src/content/organizations/Controller.tsx +++ b/src/content/organizations/Controller.tsx @@ -11,7 +11,7 @@ import OrganizationView from "./OrganizationView"; * @param {void} props - React props * @returns {FC} - React component */ -export default () => { +const OrganizationController = () => { const { orgId } = useParams<{ orgId?: string }>(); const { user } = useAuthContext(); const isAdministrative = user?.role === "Admin"; @@ -30,3 +30,5 @@ export default () => { ); }; + +export default OrganizationController; diff --git a/src/content/questionnaire/Controller.tsx b/src/content/questionnaire/Controller.tsx index c6bcb7f11..c333bdd84 100644 --- a/src/content/questionnaire/Controller.tsx +++ b/src/content/questionnaire/Controller.tsx @@ -10,7 +10,7 @@ import { FormProvider } from "../../components/Contexts/FormContext"; * @param {void} * @returns {FC} - React component */ -export default () => { +const QuestionnaireController = () => { const { appId, section } = useParams(); if (appId) { @@ -23,3 +23,5 @@ export default () => { return ; }; + +export default QuestionnaireController; diff --git a/src/content/questionnaire/sections/index.tsx b/src/content/questionnaire/sections/index.tsx index e180f8a70..a25dded1e 100644 --- a/src/content/questionnaire/sections/index.tsx +++ b/src/content/questionnaire/sections/index.tsx @@ -15,7 +15,7 @@ type Props = Omit & { * @param {Props} props * @returns {FunctionComponentElement} - Section component */ -export default ({ section, ...rest }: Props) => { +const SectionMap = ({ section, ...rest }: Props) => { const sectionName = section.toUpperCase(); const sectionConfig = config[sectionName]; @@ -31,3 +31,5 @@ export default ({ section, ...rest }: Props) => { // Note: Validation should prevent this from ever happening return createElement(() =>
Oops! Form section not found
); }; + +export default SectionMap; diff --git a/src/content/users/Controller.tsx b/src/content/users/Controller.tsx index 03ca2df20..ffed7bad5 100644 --- a/src/content/users/Controller.tsx +++ b/src/content/users/Controller.tsx @@ -40,7 +40,7 @@ const MemorizedProvider = memo(OrganizationProvider); * @param {Props} props - React props * @returns {FC} - React component */ -export default ({ type }: Props) => { +const UserController = ({ type }: Props) => { const { userId } = useParams(); const { user } = useAuthContext(); const { _id, role } = user || {}; @@ -67,3 +67,5 @@ export default ({ type }: Props) => { ); }; + +export default UserController; diff --git a/src/utils/formModeUtils.test.ts b/src/utils/formModeUtils.test.ts index ad6ca2654..9db37f6f7 100644 --- a/src/utils/formModeUtils.test.ts +++ b/src/utils/formModeUtils.test.ts @@ -314,10 +314,6 @@ describe("getFormMode tests based on provided requirements", () => { expect(utils.getFormMode(null, null)).toBe(utils.FormModes.UNAUTHORIZED); }); - it("should set Unauthorized when a null data Submission and User is provided", () => { - expect(utils.getFormMode(null, null)).toBe(utils.FormModes.UNAUTHORIZED); - }); - it("should set Unauthorized form if user role is undefined", () => { const user: User = { ...baseUser, role: undefined }; diff --git a/src/utils/stringUtils.test.ts b/src/utils/stringUtils.test.ts index 199da9785..406029a64 100644 --- a/src/utils/stringUtils.test.ts +++ b/src/utils/stringUtils.test.ts @@ -122,3 +122,37 @@ describe("filterPositiveIntegerString utility function", () => { expect(result).toEqual("1"); }); }); + +describe("titleCase", () => { + it("should capitalize the first letter of each word", () => { + expect(utils.titleCase("data file")).toBe("Data File"); + }); + + it("should handle single word strings", () => { + expect(utils.titleCase("participant")).toBe("Participant"); + }); + + it("should handle empty strings", () => { + expect(utils.titleCase("")).toBe(""); + }); + + it("should safely handle null values", () => { + expect(utils.titleCase(null)).toBe(""); + }); + + it("should safely handle undefined values", () => { + expect(utils.titleCase(undefined)).toBe(""); + }); + + it("should safely handle non-string values", () => { + expect(utils.titleCase(["this isnt a string"] as unknown as string)).toBe(""); + }); + + it("should handle strings with multiple spaces", () => { + expect(utils.titleCase("data file")).toBe("Data File"); + }); + + it("should handle strings with mixed case", () => { + expect(utils.titleCase("dATa fiLE")).toBe("Data File"); + }); +}); diff --git a/src/utils/stringUtils.ts b/src/utils/stringUtils.ts index a1e755d7e..fe4f53e3e 100644 --- a/src/utils/stringUtils.ts +++ b/src/utils/stringUtils.ts @@ -8,6 +8,25 @@ export const capitalizeFirstLetter = (str: string): string => str ? str[0].toUpperCase() + str.slice(1) : ""; +/** + * Capitalizes the first letter of each word in a given string. + * + * @see Utilizes {@link capitalizeFirstLetter} to capitalize each word. + * @param str - The string to capitalize. + * @returns The capitalized string. + */ +export const titleCase = (str: string): string => { + if (typeof str !== "string") { + return ""; + } + + return str + .toLowerCase() + .split(" ") + .map((word) => capitalizeFirstLetter(word)) + .join(" "); +}; + /** * Function to add a space between a number and a letter in a string. * @param input - The input string to be processed. It should be a string where a number is directly followed by a letter.