diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d3464fe15..701219792 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -22,12 +22,9 @@ jobs: run: ./.github/build-vars.sh set_values env: CODE_CLIMATE_ID: ${{ secrets.CODE_CLIMATE_ID }} - - name: read .nvmrc - id: node_version - run: echo ::set-output name=NODE_VERSION::$(cat .nvmrc) - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v3 with: - node-version: ${{ steps.node_version.outputs.NODE_VERSION }} + node-version-file: ".nvmrc" - uses: actions/cache@v2 with: path: | @@ -89,12 +86,9 @@ jobs: run: ./.github/github-lock.sh $branch_name env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: read .nvmrc - id: node_version - run: echo ::set-output name=NODE_VERSION::$(cat .nvmrc) - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v3 with: - node-version: ${{ steps.node_version.outputs.NODE_VERSION }} + node-version-file: ".nvmrc" - uses: actions/cache@v2 with: path: | @@ -131,9 +125,11 @@ jobs: if: ${{ github.ref != 'refs/heads/production' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 + - name: Run Cypress Tests - uses: cypress-io/github-action@v4.2.0 + uses: cypress-io/github-action@v5 with: working-directory: tests/cypress spec: | @@ -148,6 +144,7 @@ jobs: CYPRESS_STATE_USER_PASSWORD: ${{ secrets.CYPRESS_STATE_USER_PASSWORD }} CYPRESS_ADMIN_USER_EMAIL: ${{ secrets.CYPRESS_ADMIN_USER_EMAIL }} CYPRESS_ADMIN_USER_PASSWORD: ${{ secrets.CYPRESS_ADMIN_USER_PASSWORD }} + - name: Upload screenshots uses: actions/upload-artifact@v2 if: failure() @@ -163,9 +160,11 @@ jobs: if: ${{ github.ref != 'refs/heads/production' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 + - name: Check Project A11y - uses: cypress-io/github-action@v4.2.0 + uses: cypress-io/github-action@v5 with: working-directory: tests/cypress spec: tests/accessibility/*.feature @@ -179,6 +178,7 @@ jobs: CYPRESS_ADMIN_USER_EMAIL: ${{ secrets.CYPRESS_ADMIN_USER_EMAIL }} CYPRESS_ADMIN_USER_PASSWORD: ${{ secrets.CYPRESS_ADMIN_USER_PASSWORD }} RUN_PA11Y: true + - name: Upload screenshots uses: actions/upload-artifact@v2 if: failure() diff --git a/services/ui-src/src/components/export/ExportedEntityDetailsTable.test.tsx b/services/ui-src/src/components/export/ExportedEntityDetailsTable.test.tsx index 26733ca6d..e5b8a62f0 100644 --- a/services/ui-src/src/components/export/ExportedEntityDetailsTable.test.tsx +++ b/services/ui-src/src/components/export/ExportedEntityDetailsTable.test.tsx @@ -25,8 +25,8 @@ describe("ExportedEntityDetailsTable", () => { const expectedTextContent = [ "N/A", - "mock modal overlay text field", - "1", + "mock text field", + "mock number field", "Not answered; required", ]; diff --git a/services/ui-src/src/components/index.ts b/services/ui-src/src/components/index.ts index ee65aef29..78dccc393 100644 --- a/services/ui-src/src/components/index.ts +++ b/services/ui-src/src/components/index.ts @@ -98,6 +98,7 @@ export { ModalDrawerReportPage } from "./reports/ModalDrawerReportPage"; export { ModalOverlayReportPage } from "./reports/ModalOverlayReportPage"; export { ReportPageFooter } from "./reports/ReportPageFooter"; export { ReportContext, ReportProvider } from "./reports/ReportProvider"; +export { EntityContext, EntityProvider } from "./reports/EntityProvider"; // statusing export { StatusTable } from "./statusing/StatusTable"; // tables diff --git a/services/ui-src/src/components/overlays/EntityDetailsOverlay.test.tsx b/services/ui-src/src/components/overlays/EntityDetailsOverlay.test.tsx index 91e9abb97..6f0db13cd 100644 --- a/services/ui-src/src/components/overlays/EntityDetailsOverlay.test.tsx +++ b/services/ui-src/src/components/overlays/EntityDetailsOverlay.test.tsx @@ -1,82 +1,153 @@ -import { EntityProvider } from "components/reports/EntityProvider"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +// components +import { EntityDetailsOverlay } from "./EntityDetailsOverlay"; +// utils import { - mockMlrReportContext, + mockAdminUser, + mockEntityDetailsContext, + mockMLRReportEntityStartedFieldData, + mockModalOverlayForm, + mockStateUser, RouterWrappedComponent, } from "utils/testing/setupJest"; -import { EntityDetailsOverlay } from "./EntityDetailsOverlay"; -import form from "../../forms/mlr/mlr.json"; -import { EntityType, FormJson } from "types"; -import { render } from "@testing-library/react"; -import { axe } from "jest-axe"; -import userEvent from "@testing-library/user-event"; -import { ReportContext } from "components/reports/ReportProvider"; - -const formJSON: FormJson = form.routes[1].overlayForm!; -const mockClose = jest.fn(); -const mockSidebarHidden = jest.fn(); - -const overlayProps = { - entityType: "program" as EntityType, - verbiage: {}, - form: formJSON, - selectedEntity: mockMlrReportContext.report.fieldData.program[1], - closeEntityDetailsOverlay: mockClose, - setSidebarHidden: mockSidebarHidden, -}; - -const mockUpdate = jest.fn(); -const mockedReportContext = { - ...mockMlrReportContext, - updateReport: mockUpdate, -}; - -const entityDetailsOverlay = ( +import { useUser } from "utils"; +// verbiage +import accordionVerbiage from "../../verbiage/pages/accordion"; +import overlayVerbiage from "../../verbiage/pages/overlays"; +import { EntityContext } from "components"; + +const mockCloseEntityDetailsOverlay = jest.fn(); +const mockOnSubmit = jest.fn(); + +jest.mock("utils/auth/useUser"); +const mockedUseUser = useUser as jest.MockedFunction; + +const entityDetailsOverlayComponentStateUser = ( - - - - - + + + ); -describe("Test EntityDetailsOverlay", () => { - it("Should show a close button", async () => { - const { findByText } = render(entityDetailsOverlay); - expect(await findByText("Return to MLR Reporting")).toBeVisible(); - }); +const entityDetailsOverlayComponentAdminUser = ( + + + + + +); - it("Should invoke the close function when you click the close button.", async () => { - const { findByText } = render(entityDetailsOverlay); - const closeButton = await findByText("Return to MLR Reporting"); - await userEvent.click(closeButton); - expect(mockClose).toHaveBeenCalled(); +describe("Test EntityDetailsOverlayV2 (empty state)", () => { + beforeEach(() => { + jest.clearAllMocks(); }); - it("Should set the sidebar hidden on load", () => { - render(entityDetailsOverlay); - expect(mockSidebarHidden).toHaveBeenCalledWith(true); + const user = userEvent.setup(); + const selectedEntity = mockMLRReportEntityStartedFieldData.program[0]; + + it("should render the initial view for a state user", async () => { + mockedUseUser.mockReturnValue(mockStateUser); + render(entityDetailsOverlayComponentStateUser); + + // Close out of the Overlay it opened + const closeButton = screen.getByText("Return to MLR Reporting"); + expect(closeButton).toBeVisible(); + + // Check if header is visible on load - H2 + expect( + screen.getByText(overlayVerbiage.MLR.intro.subsection) + ).toBeVisible(); + + // Check if accordion is showing + const accordionHeader = accordionVerbiage.MLR.formIntro.buttonLabel; + expect(screen.getByText(accordionHeader)).toBeVisible(); + + // Check if MLR Report For is showing the correct Entity Data + const reportPlanName = selectedEntity.report_planName; + const reportProgramName = selectedEntity.report_programName; + const eligibilityGroup = selectedEntity.report_eligibilityGroup[0].value; + const reportingPeriod = `${selectedEntity.report_reportingPeriodStartDate} to ${selectedEntity.report_reportingPeriodEndDate}`; + + expect(screen.getByText(reportPlanName)).toBeVisible(); + expect(screen.getByText(reportProgramName)).toBeVisible(); + expect(screen.getByText(eligibilityGroup)).toBeVisible(); + expect(screen.getByText(reportingPeriod)).toBeVisible(); + + // Make sure footer button appears correctly + const saveAndReturn = screen.getByText("Save & return"); + expect(saveAndReturn).toBeVisible(); }); - it("Should set the sidebar visible on unmount", () => { - const { unmount } = render(entityDetailsOverlay); - unmount(); - expect(mockSidebarHidden).toHaveBeenCalledWith(false); + it("should render the initial view for an admin", async () => { + mockedUseUser.mockReturnValue(mockAdminUser); + render(entityDetailsOverlayComponentAdminUser); + + // Close out of the Overlay it opened + const closeButton = screen.getByText("Return to MLR Reporting"); + expect(closeButton).toBeVisible(); + + // Check if header is visible on load - H2 + expect( + screen.getByText(overlayVerbiage.MLR.intro.subsection) + ).toBeVisible(); + + // Check if accordion is showing + const accordionHeader = accordionVerbiage.MLR.formIntro.buttonLabel; + expect(screen.getByText(accordionHeader)).toBeVisible(); + + // Check if MLR Report For is showing the correct Entity Data + const reportPlanName = selectedEntity.report_planName; + const reportProgramName = selectedEntity.report_programName; + const eligibilityGroup = selectedEntity.report_eligibilityGroup[0].value; + const reportingPeriod = `${selectedEntity.report_reportingPeriodStartDate} to ${selectedEntity.report_reportingPeriodEndDate}`; + + expect(screen.getByText(reportPlanName)).toBeVisible(); + expect(screen.getByText(reportProgramName)).toBeVisible(); + expect(screen.getByText(eligibilityGroup)).toBeVisible(); + expect(screen.getByText(reportingPeriod)).toBeVisible(); + + // Make sure footer button appears correctly for admins + const returnButton = screen.getByText("Return"); + expect(returnButton).toBeVisible(); }); - it("Should submit entity info when clicking submit", async () => { - const { findByText } = render(entityDetailsOverlay); - const submitButton = await findByText("Save & return"); - await userEvent.click(submitButton); - expect(mockSidebarHidden).toHaveBeenCalledWith(false); - expect(mockClose).toHaveBeenCalled(); + it("should call the close overlay function when clicking Return to MLR", async () => { + // Set as State User + mockedUseUser.mockReturnValue(mockStateUser); + render(entityDetailsOverlayComponentStateUser); + + // Close out of the Overlay it opened + const closeButton = screen.getByText("Return to MLR Reporting"); + await user.click(closeButton); + expect(mockCloseEntityDetailsOverlay).toBeCalled(); }); -}); -describe("Test EntityDetailsOverlay accessibility", () => { - it("Should not have basic accessibility issues", async () => { - const { container } = render(entityDetailsOverlay); - const results = await axe(container); - expect(results).toHaveNoViolations(); + it("should call the close overlay function when clicking Return to MLR as an Admin", async () => { + // Set as State User + mockedUseUser.mockReturnValue(mockAdminUser); + render(entityDetailsOverlayComponentAdminUser); + + // Close out of the Overlay it opened + const closeButton = screen.getByText("Return to MLR Reporting"); + await user.click(closeButton); + expect(mockCloseEntityDetailsOverlay).toBeCalled(); }); }); diff --git a/services/ui-src/src/components/overlays/EntityDetailsOverlay.tsx b/services/ui-src/src/components/overlays/EntityDetailsOverlay.tsx index 6b7dc7c19..2a00c72f9 100644 --- a/services/ui-src/src/components/overlays/EntityDetailsOverlay.tsx +++ b/services/ui-src/src/components/overlays/EntityDetailsOverlay.tsx @@ -1,176 +1,123 @@ -import { useContext, useEffect, useState } from "react"; -import arrowLeftBlue from "assets/icons/icon_arrow_left_blue.png"; +import React, { MouseEventHandler, useContext, useEffect } from "react"; // components -import { Form, ReportContext, ReportPageIntro } from "components"; -import { Box, Button, Flex, Image, Text, Spinner } from "@chakra-ui/react"; +import { Box, Button, Flex, Image, Spinner, Text } from "@chakra-ui/react"; +import { Form, ReportPageIntro } from "components"; +// types +import { EntityShape, EntityType, FormJson } from "types"; // utils -import { - AnyObject, - EntityShape, - EntityType, - FormJson, - isFieldElement, - ReportStatus, -} from "types"; -import { filterFormData, useUser } from "utils"; + +// assets +import arrowLeftBlue from "assets/icons/icon_arrow_left_blue.png"; +// verbiage import accordionVerbiage from "../../verbiage/pages/accordion"; import overlayVerbiage from "../../verbiage/pages/overlays"; import { EntityContext } from "components/reports/EntityProvider"; + export const EntityDetailsOverlay = ({ + closeEntityDetailsOverlay, entityType, + entities, form, - verbiage, + onSubmit, selectedEntity, - closeEntityDetailsOverlay, - setSidebarHidden, + disabled, + submitting, validateOnRender, }: Props) => { - const [submitting, setSubmitting] = useState(false); - const { report, updateReport } = useContext(ReportContext); - const { full_name, state } = useUser().user ?? {}; - const onError = () => {}; - const { - entities, - updateEntities, - setEntities, - setSelectedEntity, - setEntityType, - } = useContext(EntityContext); + // Entity Provider Setup + const { setEntities, setSelectedEntity, setEntityType } = + useContext(EntityContext); useEffect(() => { setSelectedEntity(selectedEntity); - setSidebarHidden(true); setEntityType(entityType); - setEntities(report?.fieldData[entityType]); + setEntities(entities); return () => { setEntities([]); setSelectedEntity(undefined); - setSidebarHidden(false); }; }, [entityType, selectedEntity]); - const onSubmit = async (enteredData: AnyObject) => { - setSubmitting(true); - const filteredFormData = filterFormData( - enteredData, - form.fields.filter(isFieldElement) - ); - const newEntity = { - ...selectedEntity, - ...filteredFormData, - }; - updateEntities(newEntity); - const reportKeys = { - reportType: report?.reportType, - state: state, - id: report?.id, - }; - const dataToWrite = { - metadata: { - status: ReportStatus.IN_PROGRESS, - lastAlteredBy: full_name, - }, - fieldData: { - program: entities, - }, - }; - await updateReport(reportKeys, dataToWrite); - setSubmitting(false); - closeEntityDetailsOverlay(); - setSidebarHidden(false); - }; - - const closeOverlay = () => { - setSidebarHidden(true); - closeEntityDetailsOverlay(); - }; - - const { report_programName, report_planName } = selectedEntity; - + // Display Variables + const { + report_programName: reportProgramName, + report_planName: reportPlanName, + } = selectedEntity; + const eligibilityGroup = `${ + selectedEntity["report_eligibilityGroup-otherText"] || + selectedEntity.report_eligibilityGroup[0].value + }`; const reportingPeriod = `${selectedEntity.report_reportingPeriodStartDate} to ${selectedEntity.report_reportingPeriodEndDate}`; - const eligibilityGroup = () => { - if (selectedEntity && selectedEntity["report_eligibilityGroup-otherText"]) { - return selectedEntity["report_eligibilityGroup-otherText"]; - } - return selectedEntity.report_eligibilityGroup[0].value; - }; - - const { userIsEndUser } = useUser().user ?? {}; const programInfo = [ - report_planName, - report_programName, - eligibilityGroup(), + reportPlanName, + reportProgramName, + eligibilityGroup, reportingPeriod, ]; return ( - - - - Arrow left - Return to MLR Reporting - - {verbiage.intro && ( - - )} - - MLR report for: -
    - {programInfo.map((field, index) => ( -
  • {field}
  • - ))} -
-
-
- - + + + + + MLR report for: +
    + {programInfo.map((field, index) => ( +
  • {field}
  • + ))} +
+
+ + + + {disabled ? ( + ) : ( + - - + )} +
); }; interface Props { + closeEntityDetailsOverlay: Function; entityType: EntityType; + entities: any; form: FormJson; - verbiage: AnyObject; + onSubmit: Function; selectedEntity: EntityShape; - closeEntityDetailsOverlay: Function; - setSidebarHidden: Function; + disabled: boolean; + submitting?: boolean; validateOnRender?: boolean; } @@ -180,6 +127,8 @@ const sx = { width: "100%", }, backButton: { + padding: 0, + fontWeight: "normal", color: "palette.primary", display: "flex", position: "relative", @@ -191,8 +140,6 @@ const sx = { color: "palette.primary", height: "1rem", marginRight: "0.5rem", - position: "relative", - top: "0.25rem", }, footerBox: { marginTop: "2rem", diff --git a/services/ui-src/src/components/reports/ModalOverlayReportPage.test.tsx b/services/ui-src/src/components/reports/ModalOverlayReportPage.test.tsx index ef338abe2..d1d0f6a88 100644 --- a/services/ui-src/src/components/reports/ModalOverlayReportPage.test.tsx +++ b/services/ui-src/src/components/reports/ModalOverlayReportPage.test.tsx @@ -1,270 +1,375 @@ -import { act, render, screen, waitFor } from "@testing-library/react"; -import { axe } from "jest-axe"; +import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; +import { axe } from "jest-axe"; // components -import { ReportContext, ModalOverlayReportPage } from "components"; +import { + EntityProvider, + ModalOverlayReportPage, + ReportContext, +} from "components"; // utils import { - mockModalOverlayReportPageJson, + RouterWrappedComponent, + mockMLRNewReportContext, mockModalOverlayReportPageWithOverlayJson, + mockMLREntityStartedReportContext, mockStateUser, - RouterWrappedComponent, - mockMlrReportContext, + mockAdminUser, } from "utils/testing/setupJest"; -import { useBreakpoint, makeMediaQueryClasses, useUser } from "utils"; - -const mockUseNavigate = jest.fn(); -jest.mock("react-router-dom", () => ({ - useNavigate: () => mockUseNavigate, - useLocation: jest.fn(() => ({ - pathname: "/mock-route", - })), -})); - -jest.mock("utils/other/useBreakpoint"); -const mockUseBreakpoint = useBreakpoint as jest.MockedFunction< - typeof useBreakpoint ->; -const mockMakeMediaQueryClasses = makeMediaQueryClasses as jest.MockedFunction< - typeof makeMediaQueryClasses ->; +import { useUser } from "utils"; +// verbiage +import accordionVerbiage from "../../verbiage/pages/accordion"; jest.mock("utils/auth/useUser"); const mockedUseUser = useUser as jest.MockedFunction; -const { addEntityButtonText, deleteModalConfirmButtonText } = - mockModalOverlayReportPageJson.verbiage; - -const mockReportContextWithoutEntities = { - ...mockMlrReportContext, - report: undefined, -}; - -const mockReportWithCompletedEntityContext = { - ...mockMlrReportContext, - report: { - ...mockMlrReportContext.report, - fieldData: { - ...mockMlrReportContext.report.fieldData, - program: [ - { - id: "123", - name: "example-program1", - report_eligibilityGroup: [ - { - key: "option1", - value: "mock-option", - }, - ], - "report_eligibilityGroup-otherText": "mock-text", - }, - ], - }, - }, -}; - -const modalOverlayReportPageComponent = ( +const mockSetSidebarHidden = jest.fn(); + +const modalOverlayReportPageInitialComponent = ( - + ); -const modalOverlayReportPageComponentWithEntities = ( +const modalOverlayReportPageEntityAddedComponent = ( - - + + + + ); -describe("Test ModalOverlayReportPage (empty state, desktop)", () => { +describe("Test ModalOverlayReportPage (empty state)", () => { beforeEach(() => { - mockUseBreakpoint.mockReturnValue({ - isMobile: false, - isTablet: false, - }); - mockMakeMediaQueryClasses.mockReturnValue("desktop"); + jest.clearAllMocks(); + }); + + const verbiage = mockModalOverlayReportPageWithOverlayJson.verbiage; + + it("should render the initial view for a State user", () => { + // Set as State User mockedUseUser.mockReturnValue(mockStateUser); - render(modalOverlayReportPageComponent); + render(modalOverlayReportPageInitialComponent); + + // Check if header is visible on load - H1 + expect(screen.getByText(verbiage.intro.section)).toBeVisible(); + // Check if header is visible on load - H2 + expect(screen.getByText(verbiage.intro.subsection)).toBeVisible(); + + // Check if accordion is showing + const accordionHeader = accordionVerbiage.MLR.formIntro.buttonLabel; + expect(screen.getByText(accordionHeader)).toBeVisible(); + + // Check if dashboard title is showing 0 entities + const dashboardTitle = `${verbiage.dashboardTitle} 0`; + expect(screen.getByText(dashboardTitle)).toBeVisible(); + + // Check if emptyDashboardText is displaying + const emptyDashboardText = verbiage.emptyDashboardText; + expect(screen.getByText(emptyDashboardText)).toBeVisible(); + + // Check if addEntity button is displaying + const addInformationButton = verbiage.addEntityButtonText; + expect(screen.getByText(addInformationButton)).toBeVisible(); + + // Check if Footer is display with a next button and no previous butto + expect(screen.getByText("Continue")).toBeVisible(); + expect(screen.getByText("Previous")).toBeVisible(); }); - it("should render the view", () => { - expect( - screen.getByText( - mockModalOverlayReportPageWithOverlayJson.verbiage.intro.section - ) - ).toBeVisible(); + it("should open the add/edit modal for a State User", async () => { + // Set as State User + mockedUseUser.mockReturnValue(mockStateUser); + render(modalOverlayReportPageInitialComponent); + + const user = userEvent.setup(); + + // Get the Add button and click it + const addEntityButton = screen.getByText(verbiage.addEntityButtonText); + await user.click(addEntityButton); + expect(screen.getByRole("dialog")).toBeVisible(); + + // Close out of the modal it created + const closeButton = screen.getByText("Close"); + await user.click(closeButton); }); + + it("should render the initial view for a Admin user", () => { + // Set as State User + mockedUseUser.mockReturnValue(mockAdminUser); + render(modalOverlayReportPageInitialComponent); + + // Check if header is visible on load - H1 + expect(screen.getByText(verbiage.intro.section)).toBeVisible(); + // Check if header is visible on load - H2 + expect(screen.getByText(verbiage.intro.subsection)).toBeVisible(); + + // Check if accordion is showing + const accordionHeader = accordionVerbiage.MLR.formIntro.buttonLabel; + expect(screen.getByText(accordionHeader)).toBeVisible(); + + // Check if dashboard title is showing 0 entities + const dashboardTitle = `${verbiage.dashboardTitle} 0`; + expect(screen.getByText(dashboardTitle)).toBeVisible(); + + // Check if emptyDashboardText is displaying + const emptyDashboardText = verbiage.emptyDashboardText; + expect(screen.getByText(emptyDashboardText)).toBeVisible(); + + // Check if addEntity button is displaying but disabled + const addInformationButton = verbiage.addEntityButtonText; + expect(screen.getByText(addInformationButton)).toBeVisible(); + expect(screen.getByText(addInformationButton)).toBeDisabled(); + + // Check if Footer is display with a next button and no previous butto + expect(screen.getByText("Continue")).toBeVisible(); + expect(screen.getByText("Previous")).toBeVisible(); + }); + /** + * @todo Write a test to make sure admins can't click/change details? + */ }); -describe("Test ModalOverlayReportPage (empty state, mobile & tablet)", () => { +describe("Test ModalOverlayReportPage (Entities Added State)", () => { beforeEach(() => { - mockUseBreakpoint.mockReturnValue({ - isMobile: true, - isTablet: true, - }); - mockMakeMediaQueryClasses.mockReturnValue("mobile"); + jest.clearAllMocks(); + }); + + const verbiage = mockModalOverlayReportPageWithOverlayJson.verbiage; + + it("should render the initial view for a State user", () => { + // Set as State User mockedUseUser.mockReturnValue(mockStateUser); - render(modalOverlayReportPageComponent); + render(modalOverlayReportPageEntityAddedComponent); + + // Check if header is visible on load - H1 + expect(screen.getByText(verbiage.intro.section)).toBeVisible(); + // Check if header is visible on load - H2 + expect(screen.getByText(verbiage.intro.subsection)).toBeVisible(); + + // Check if accordion is showing + const accordionHeader = accordionVerbiage.MLR.formIntro.buttonLabel; + expect(screen.getByText(accordionHeader)).toBeVisible(); + + // Check if dashboard title is showing 1 entities + const dashboardTitle = `${verbiage.dashboardTitle} 1`; + expect(screen.getByText(dashboardTitle)).toBeVisible(); + + // Check if emptyDashboardText is NOT displaying + const emptyDashboardText = verbiage.emptyDashboardText; + expect(screen.queryByText(emptyDashboardText)).toBeNull(); + + // Check if action buttons are visible + const editEntityButton = screen.getByText(verbiage.editEntityButtonText); + const deleteEntityButton = screen.getByTestId("delete-entity"); + expect(editEntityButton).toBeVisible(); + expect(deleteEntityButton).toBeVisible(); + + // Check if addEntity button is displaying + const addInformationButton = verbiage.addEntityButtonText; + expect(screen.getByText(addInformationButton)).toBeVisible(); + + // Check if Footer is display with a next button and no previous butto + expect(screen.getByText("Continue")).toBeVisible(); + expect(screen.getByText("Previous")).toBeVisible(); }); - afterAll(() => { - jest.clearAllMocks(); + it("should open the edit modal", async () => { + // Setup as a State User + mockedUseUser.mockReturnValue(mockStateUser); + render(modalOverlayReportPageEntityAddedComponent); + + const user = userEvent.setup(); + + // Get the Edit button and click it + const editEntityButton = screen.getByText(verbiage.editEntityButtonText); + await user.click(editEntityButton); + expect(screen.getByRole("dialog")).toBeVisible(); + + // Close out of the modal it created + const closeButton = screen.getByText("Close"); + await user.click(closeButton); + + // And make sure they can still add entities + const addEntityButton = screen.getByText(verbiage.addEntityButtonText); + expect(addEntityButton).toBeVisible(); }); - it("should render the view", () => { + it("should open and close the delete modal as a State user", async () => { + //Setup as a state user + mockedUseUser.mockReturnValue(mockStateUser); + render(modalOverlayReportPageEntityAddedComponent); + + const user = userEvent.setup(); + + // Verify the entity exists + expect(screen.getByRole("table")).not.toBeNull; expect( screen.getByText( - mockModalOverlayReportPageWithOverlayJson.verbiage.intro.section + mockMLREntityStartedReportContext.report.fieldData.program[0] + .report_planName ) ).toBeVisible(); - }); -}); -describe("Test ModalOverlayReportPage (desktop, adding new program reporting information)", () => { - beforeEach(() => { - mockUseBreakpoint.mockReturnValue({ - isMobile: false, - isTablet: false, - }); - mockMakeMediaQueryClasses.mockReturnValue("desktop"); - mockedUseUser.mockReturnValue(mockStateUser); - act(() => { - render(modalOverlayReportPageComponentWithEntities); - }); - }); + // Get the Delete button and click it + const deleteEntityButton = screen.getByTestId("delete-entity"); + await userEvent.click(deleteEntityButton); + expect(screen.getByRole("dialog")).toBeVisible(); - it("State user should be able to enter an existing program", async () => { - const enterButton = screen.getByText("Mock enter report text"); - await userEvent.click(enterButton); - await waitFor( - () => { - expect(screen.getByText("Return to MLR Reporting")); - }, - { - timeout: 1000, - } + // Click delete in modal + const deleteButton = screen.getByText( + verbiage.deleteModalConfirmButtonText ); + await user.click(deleteButton); + + // Close out of the modal it created + const closeButton = screen.getByText("Close"); + await user.click(closeButton); + + // Verify that the entity is removed + expect(screen.getByRole("table")).toBeNull; + + // And make sure they can still add entities + const addEntityButton = screen.getByText(verbiage.addEntityButtonText); + expect(addEntityButton).toBeVisible(); }); - it("State user should be able to open and close the Add Program Reporting Information modal", async () => { - // open the modal - const addEntityButton = screen.getByText(addEntityButtonText); - await userEvent.click(addEntityButton); - expect(screen.getByRole("dialog")).toBeVisible(); + it("should be unable to click the delete button as an Admin", async () => { + //Setup as a state user + mockedUseUser.mockReturnValue(mockAdminUser); + render(modalOverlayReportPageEntityAddedComponent); - // close the modal - const closeButton = screen.getByText("Close"); - await userEvent.click(closeButton); + // Verify the entity exists + expect(screen.getByRole("table")).not.toBeNull; expect( screen.getByText( - mockModalOverlayReportPageWithOverlayJson.verbiage.intro.section + mockMLREntityStartedReportContext.report.fieldData.program[0] + .report_planName ) ).toBeVisible(); + + // Get the Delete button and click it + const deleteEntityButton = screen.getByTestId("delete-entity"); + expect(deleteEntityButton).toBeDisabled(); }); - it("State user should be able to delete existing entities", async () => { - // verify program table exists - expect(screen.getByRole("table")).not.toBeNull; + it("should open and close the overlay page as a State user", async () => { + //Setup as a state user + mockedUseUser.mockReturnValue(mockStateUser); + render(modalOverlayReportPageEntityAddedComponent); - // delete program entity - const closeButton = screen.getByRole("button", { name: "delete icon" }); - await userEvent.click(closeButton); - expect(screen.getByRole("dialog")).toBeVisible(); + const user = userEvent.setup(); - // click delete in modal - const deleteButton = screen.getByRole("button", { - name: deleteModalConfirmButtonText, - }); - await userEvent.click(deleteButton); + // Check if dashboard title is showing 1 entities + const dashboardTitle = `${verbiage.dashboardTitle} 1`; + expect(screen.getByText(dashboardTitle)).toBeVisible(); - // verify that the program is removed - expect(screen.getByRole("table")).toBeNull; + // Get the Enter button and click it + const enterEntityButton = screen.getByText(verbiage.enterReportText); + await user.click(enterEntityButton); + + expect(mockSetSidebarHidden).toBeCalledWith(true); + + // Close out of the Overlay it opened + const closeButton = screen.getByText("Return to MLR Reporting"); + await user.click(closeButton); + + // And make sure we're back on the first page! + expect(screen.getByText(dashboardTitle)).toBeVisible(); }); -}); -describe("Test ModalOverlayReportPage (mobile + tablet, adding new program reporting information)", () => { - beforeEach(() => { - mockUseBreakpoint.mockReturnValue({ - isMobile: true, - isTablet: true, - }); - mockMakeMediaQueryClasses.mockReturnValue("mobile"); + it("should submit the form when a State user opens an entity and adds information", async () => { + window.HTMLElement.prototype.scrollIntoView = function () {}; + + //Setup as a state user mockedUseUser.mockReturnValue(mockStateUser); - render(modalOverlayReportPageComponentWithEntities); - }); + render(modalOverlayReportPageEntityAddedComponent); - it("State user should be able to enter an existing program", async () => { - const enterButton = screen.getByText("Mock enter report text"); - await userEvent.click(enterButton); - await waitFor( - () => { - expect(screen.getByText("Return to MLR Reporting")); - }, - { - timeout: 1000, - } - ); - }); + const user = userEvent.setup(); - it("State user should be able to open and close the Add Program Reporting Information modal", async () => { - // open the modal - const addEntityButton = screen.getByText(addEntityButtonText); - await userEvent.click(addEntityButton); - expect(screen.getByRole("dialog")).toBeVisible(); + // Check if dashboard title is showing 1 entities + const dashboardTitle = `${verbiage.dashboardTitle} 1`; + expect(screen.getByText(dashboardTitle)).toBeVisible(); - // close the modal - const closeButton = screen.getByText("Close"); - await userEvent.click(closeButton); + // Get the Enter button and click it + const enterEntityButton = screen.getByText(verbiage.enterReportText); + await user.click(enterEntityButton); + + expect(mockSetSidebarHidden).toBeCalledWith(true); + + // Test text fields + const textField = screen.getByLabelText("mock text field"); + expect(textField).toBeVisible(); + await userEvent.type(textField, "test"); + + // Test number fields + const numberField = screen.getByLabelText("mock number field"); + expect(numberField).toBeVisible(); + await userEvent.type(numberField, "123"); + + const saveAndCloseButton = screen.getByText("Save & return"); + await userEvent.click(saveAndCloseButton); + + // Will be 3 times! Twice for autosave and once for clicking the button expect( - screen.getByText( - mockModalOverlayReportPageWithOverlayJson.verbiage.intro.section - ) - ).toBeVisible(); + mockMLREntityStartedReportContext.updateReport + ).toHaveBeenCalledTimes(3); + + // And make sure we're back on the first page! + expect(mockSetSidebarHidden).toBeCalledWith(false); + expect(screen.getByText(dashboardTitle)).toBeVisible(); }); - it("State user should be able to delete existing entities", async () => { - // verify program table exists - expect(screen.getByRole("table")).not.toBeNull; + it("should be able to open an entity by not submit as an admin", async () => { + //Setup as a state user + mockedUseUser.mockReturnValue(mockAdminUser); + render(modalOverlayReportPageEntityAddedComponent); - // delete program entity - const closeButton = screen.getByRole("button", { name: "delete icon" }); - await userEvent.click(closeButton); - expect(screen.getByRole("dialog")).toBeVisible(); + const user = userEvent.setup(); - // click delete in modal - const deleteButton = screen.getByRole("button", { - name: deleteModalConfirmButtonText, - }); - await userEvent.click(deleteButton); + // Check if dashboard title is showing 1 entities + const dashboardTitle = `${verbiage.dashboardTitle} 1`; + expect(screen.getByText(dashboardTitle)).toBeVisible(); - // verify that the program is removed - expect(screen.getByRole("table")).toBeNull; + // Get the Enter button and click it + const enterEntityButton = screen.getByText(verbiage.enterReportText); + await user.click(enterEntityButton); + expect(mockSetSidebarHidden).toBeCalledWith(true); + + const saveAndCloseButton = screen.getByText("Return"); + await userEvent.click(saveAndCloseButton); + + expect( + mockMLREntityStartedReportContext.updateReport + ).toHaveBeenCalledTimes(0); + + // And make sure we're back on the first page! + expect(mockSetSidebarHidden).toBeCalledWith(false); + expect(screen.getByText(dashboardTitle)).toBeVisible(); }); }); describe("Test ModalOverlayReportPage accessibility", () => { - beforeEach(() => { + it("Should not have basic accessibility issues", async () => { mockedUseUser.mockReturnValue(mockStateUser); - render(modalOverlayReportPageComponent); - }); - - it("Should not have basic accessibility issues (desktop)", async () => { - const { container } = render(modalOverlayReportPageComponent); + const { container } = render(modalOverlayReportPageInitialComponent); const results = await axe(container); expect(results).toHaveNoViolations(); }); - it("Should not have basic accessibility issues (mobile)", async () => { - const { container } = render(modalOverlayReportPageComponent); + it("Should not have basic accessibility issues", async () => { + mockedUseUser.mockReturnValue(mockStateUser); + const { container } = render(modalOverlayReportPageEntityAddedComponent); const results = await axe(container); expect(results).toHaveNoViolations(); }); diff --git a/services/ui-src/src/components/reports/ModalOverlayReportPage.tsx b/services/ui-src/src/components/reports/ModalOverlayReportPage.tsx index 7e38bf1aa..7c71316f0 100644 --- a/services/ui-src/src/components/reports/ModalOverlayReportPage.tsx +++ b/services/ui-src/src/components/reports/ModalOverlayReportPage.tsx @@ -5,6 +5,7 @@ import { AddEditEntityModal, DeleteEntityModal, EntityDetailsOverlay, + EntityProvider, EntityRow, MobileEntityRow, ReportContext, @@ -13,46 +14,58 @@ import { Table, } from "components"; // types -import { EntityShape, EntityType, ModalOverlayReportPageShape } from "types"; - +import { + AnyObject, + EntityShape, + EntityType, + isFieldElement, + ModalOverlayReportPageShape, + ReportStatus, +} from "types"; // utils -import { useBreakpoint, useUser } from "utils"; - +import { + entityWasUpdated, + filterFormData, + getEntriesToClear, + setClearedEntriesToDefaultValue, + useBreakpoint, + useUser, +} from "utils"; // verbiage import accordionVerbiage from "../../verbiage/pages/accordion"; -import { EntityProvider } from "./EntityProvider"; export const ModalOverlayReportPage = ({ route, setSidebarHidden, validateOnRender, }: Props) => { - const { isTablet, isMobile } = useBreakpoint(); - + // Route Information const { entityType, verbiage, modalForm, overlayForm } = route; - const [selectedEntity, setSelectedEntity] = useState( + + // Context Information + const { isTablet, isMobile } = useBreakpoint(); + const { report, updateReport } = useContext(ReportContext); + const [isEntityDetailsOpen, setIsEntityDetailsOpen] = useState(); + const [currentEntity, setCurrentEntity] = useState( undefined ); - const [isEntityDetailsOpen, setIsEntityDetailsOpen] = useState(); - const { report } = useContext(ReportContext); - const reportType = report?.reportType; - const reportFieldDataEntities = report?.fieldData[entityType] || []; - const { userIsAdmin, userIsReadOnly } = useUser().user ?? {}; - const isAdminUserType = userIsAdmin || userIsReadOnly; - const formIsDisabled = isAdminUserType && route.modalForm?.adminDisabled; - // is MLR report in a LOCKED state - const isLocked = report?.locked || formIsDisabled; + const [submitting, setSubmitting] = useState(false); + const { userIsAdmin, userIsReadOnly, userIsEndUser, full_name, state } = + useUser().user ?? {}; - const dashTitle = `${verbiage.dashboardTitle}${ - verbiage.countEntitiesInTitle ? ` ${reportFieldDataEntities.length}` : "" - }`; + // Determine whether form is locked or unlocked based on user and route + const isAdminUserType = userIsAdmin || userIsReadOnly; + const isLocked = report?.locked || isAdminUserType; + // Display Variables + const reportFieldDataEntities = report?.fieldData[entityType] || []; + const dashTitle = `${verbiage.dashboardTitle} ${reportFieldDataEntities.length}`; const tableHeaders = () => { if (isTablet || isMobile) return { headRow: ["", ""] }; return { headRow: ["", verbiage.tableHeader, ""] }; }; - // add/edit entity modal disclosure and methods + // Add/edit entity modal disclosure and methods const { isOpen: addEditEntityModalIsOpen, onOpen: addEditEntityModalOnOpenHandler, @@ -60,16 +73,16 @@ export const ModalOverlayReportPage = ({ } = useDisclosure(); const openAddEditEntityModal = (entity?: EntityShape) => { - if (entity) setSelectedEntity(entity); + if (entity) setCurrentEntity(entity); addEditEntityModalOnOpenHandler(); }; const closeAddEditEntityModal = () => { - setSelectedEntity(undefined); + setCurrentEntity(undefined); addEditEntityModalOnCloseHandler(); }; - // delete modal disclosure and methods + // Delete entity modal disclosure and methods const { isOpen: deleteEntityModalIsOpen, onOpen: deleteEntityModalOnOpenHandler, @@ -77,89 +90,138 @@ export const ModalOverlayReportPage = ({ } = useDisclosure(); const openDeleteEntityModal = (entity: EntityShape) => { - setSelectedEntity(entity); + setCurrentEntity(entity); deleteEntityModalOnOpenHandler(); }; const closeDeleteEntityModal = () => { - setSelectedEntity(undefined); + setCurrentEntity(undefined); deleteEntityModalOnCloseHandler(); }; - // entity overlay disclosure and methods - + // Open/Close overlay action methods const openEntityDetailsOverlay = (entity: EntityShape) => { - setSelectedEntity(entity); + setCurrentEntity(entity); setIsEntityDetailsOpen(true); setSidebarHidden(true); }; const closeEntityDetailsOverlay = () => { - setSelectedEntity(undefined); + setCurrentEntity(undefined); setIsEntityDetailsOpen(false); setSidebarHidden(false); }; + // Form submit methods + const onSubmit = async (enteredData: AnyObject) => { + if (userIsEndUser) { + setSubmitting(true); + const reportKeys = { + reportType: report?.reportType, + state: state, + id: report?.id, + }; + const currentEntities = [...(report?.fieldData[entityType] || [])]; + const selectedEntityIndex = report?.fieldData[entityType].findIndex( + (entity: EntityShape) => entity.id === currentEntity?.id + ); + const filteredFormData = filterFormData( + enteredData, + overlayForm!.fields.filter(isFieldElement) + ); + const entriesToClear = getEntriesToClear( + enteredData, + overlayForm!.fields.filter(isFieldElement) + ); + const newEntity = { + ...currentEntity, + ...filteredFormData, + }; + let newEntities = currentEntities; + newEntities[selectedEntityIndex] = newEntity; + newEntities[selectedEntityIndex] = setClearedEntriesToDefaultValue( + newEntities[selectedEntityIndex], + entriesToClear + ); + const shouldSave = entityWasUpdated( + reportFieldDataEntities[selectedEntityIndex], + newEntity + ); + if (shouldSave) { + const dataToWrite = { + metadata: { + status: ReportStatus.IN_PROGRESS, + lastAlteredBy: full_name, + }, + fieldData: { + [entityType]: newEntities, + }, + }; + await updateReport(reportKeys, dataToWrite); + } + setSubmitting(false); + } + closeEntityDetailsOverlay(); + setSidebarHidden(false); + }; + return ( - {overlayForm && selectedEntity && isEntityDetailsOpen ? ( + {overlayForm && isEntityDetailsOpen && currentEntity ? ( ) : ( - {verbiage.intro && ( - - )} + + {dashTitle} {reportFieldDataEntities.length === 0 ? ( <> -
+ {verbiage.emptyDashboardText} ) : ( - - {reportFieldDataEntities.map( - (entity: EntityShape, entityIndex: number) => - isMobile || isTablet ? ( - - ) : ( - - ) +
+ {reportFieldDataEntities.map((entity: EntityShape) => + isMobile || isTablet ? ( + + ) : ( + + ) )}
)} @@ -170,31 +232,30 @@ export const ModalOverlayReportPage = ({ > {verbiage.addEntityButtonText} - {report && ( - <> - - - - )}
- + + + + + +
)}
@@ -225,7 +286,16 @@ const sx = { paddingBottom: "0", }, }, - header: { + emptyDashboard: { + paddingTop: "1rem", + }, + tableSeparator: { + borderTop: "1px solid", + borderColor: "palette.gray_light", + paddingBottom: "1rem", + marginTop: "1.25rem", + }, + table: { tableLayout: "fixed", br: { marginBottom: "0.25rem", @@ -238,19 +308,16 @@ const sx = { ".tablet &, .mobile &": { border: "none", }, - "&:nth-child(1)": { + "&:nth-of-type(1)": { width: "2.5rem", }, - "&:nth-child(3)": { + "&:nth-of-type(3)": { width: "260px", }, }, }, - emptyDashboard: { - paddingTop: "2rem", - }, addEntityButton: { - marginTop: "1.5rem", + marginTop: "2rem", marginBottom: "2rem", ".tablet &, .mobile &": { wordBreak: "break-word", diff --git a/services/ui-src/src/components/reports/ReportPageFooter.tsx b/services/ui-src/src/components/reports/ReportPageFooter.tsx index c48200db8..39b6cc1e7 100644 --- a/services/ui-src/src/components/reports/ReportPageFooter.tsx +++ b/services/ui-src/src/components/reports/ReportPageFooter.tsx @@ -10,18 +10,14 @@ import { FormJson } from "types"; import nextIcon from "assets/icons/icon_next_white.png"; import previousIcon from "assets/icons/icon_previous_blue.png"; -export const ReportPageFooter = ({ - submitting, - form, - hidePrevious, - ...props -}: Props) => { +export const ReportPageFooter = ({ submitting, form, ...props }: Props) => { const navigate = useNavigate(); const { report } = useContext(ReportContext); const { previousRoute, nextRoute } = useFindRoute( report?.formTemplate.flatRoutes, report?.formTemplate.basePath ); + const hidePrevious = previousRoute === "/mcpar" || previousRoute === "/mlr"; const { userIsAdmin, userIsReadOnly } = useUser().user ?? {}; const isAdminUserType = userIsAdmin || userIsReadOnly; @@ -78,7 +74,6 @@ export const ReportPageFooter = ({ interface Props { form?: FormJson; submitting?: boolean; - hidePrevious?: boolean; [key: string]: any; } diff --git a/services/ui-src/src/components/reports/StandardReportPage.tsx b/services/ui-src/src/components/reports/StandardReportPage.tsx index e9afb221b..2a9d61cc1 100644 --- a/services/ui-src/src/components/reports/StandardReportPage.tsx +++ b/services/ui-src/src/components/reports/StandardReportPage.tsx @@ -14,7 +14,6 @@ import { AnyObject, isFieldElement, ReportStatus, - ReportType, StandardReportPageShape, } from "types"; @@ -74,11 +73,7 @@ export const StandardReportPage = ({ route, validateOnRender }: Props) => { validateOnRender={validateOnRender || false} dontReset={false} /> - +
); }; diff --git a/services/ui-src/src/components/tables/EntityRow.tsx b/services/ui-src/src/components/tables/EntityRow.tsx index a2a6b4971..24e07682e 100644 --- a/services/ui-src/src/components/tables/EntityRow.tsx +++ b/services/ui-src/src/components/tables/EntityRow.tsx @@ -71,6 +71,7 @@ export const EntityRow = ({