Skip to content

Commit

Permalink
Show loading indicator and announce loading if loading has not finish…
Browse files Browse the repository at this point in the history
…ed within a timeout
  • Loading branch information
lortimer committed Sep 2, 2023
1 parent a6d5af8 commit 811f0a5
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 11 deletions.
4 changes: 2 additions & 2 deletions src/contexts/LoadingContext.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { createContext, Dispatch, SetStateAction, useCallback } from "react";
import React, { createContext, useCallback } from "react";
import { ChildrenProps } from "../utilities/children-props";

type LoadingContextValue = {
Expand All @@ -14,7 +14,7 @@ const defaultValue: LoadingContextValue = {
export const LoadingContext = createContext<LoadingContextValue>(defaultValue);

type LoadingContextProviderProps = ChildrenProps & {
setIsLoading: Dispatch<SetStateAction<boolean>>
setIsLoading: (value: boolean) => void
}

export const LoadingContextProvider = ({ children, setIsLoading }: LoadingContextProviderProps) => {
Expand Down
64 changes: 61 additions & 3 deletions src/pages/Page.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { Page } from "./Page";
import { render, screen, waitFor } from "@testing-library/react";
import React, { useCallback, useState } from "react";
import { render, screen, waitFor, within } from "@testing-library/react";
import React, { useCallback, useContext, useState } from "react";
import { DocumentStateContext } from "../contexts/DocumentStateContext";
import { ChildrenProps } from "../utilities/children-props";
import { useFocusHashOrMainHeading } from "../hooks/focus/useFocusHashOrMainHeading";
import { LoadingContext } from "../contexts/LoadingContext";
import { UserEvent } from "@testing-library/user-event/dist/types/setup/setup";
import userEvent from "@testing-library/user-event";
import { asyncTimeout } from "../../test/async-timeout";

jest.mock("react-router-dom");
jest.mock("../hooks/focus/useFocusHashOrMainHeading");
Expand All @@ -12,9 +16,11 @@ let documentStateUpdated: boolean;

describe(Page.name, () => {
const pageTitle = "The Page";
let user: UserEvent;

beforeEach(() => {
documentStateUpdated = false;
user = userEvent.setup();
});

describe("under normal conditions", () => {
Expand All @@ -28,7 +34,6 @@ describe(Page.name, () => {
</Page>
</ContextProvider>
);

});

test("sets the document title", () => {
Expand All @@ -52,6 +57,50 @@ describe(Page.name, () => {
});
});

describe("when loading takes a long time", () => {
let liveRegion: HTMLDivElement;

beforeEach(() => {
render(
<ContextProvider freshDocument={false}>
<Page title={pageTitle}>
<h1 tabIndex={-1}>Main Heading</h1>
<ChildComponentThatLoadsData/>
</Page>
</ContextProvider>
);

liveRegion = screen.getByTestId("page-live-region");
});

test("adds loading text to live region after timeout", async () => {
await user.click(screen.getByRole("button", { name: "START LOADING" }));
const loadingText = await within(liveRegion).findByText("loading");
expect(loadingText).toBeInTheDocument();
});

test("shows a loading indicator after timeout", async () => {
await user.click(screen.getByRole("button", { name: "START LOADING" }));
const loadingText = await screen.findByText("Loading...");
expect(loadingText).toBeVisible();
});

test("does not show loading text if loading finished within the timeout period", async () => {
const originalAppLoadingTimeout = process.env.REACT_APP_LOADING_TIMEOUT;
process.env.REACT_APP_LOADING_TIMEOUT = "250";

await user.click(screen.getByRole("button", { name: "START LOADING" }));
await user.click(screen.getByRole("button", { name: "STOP LOADING" }));

await asyncTimeout(parseInt(process.env.REACT_APP_LOADING_TIMEOUT!) * 2);

const loadingText = within(liveRegion).queryByText("loading");
expect(loadingText).not.toBeInTheDocument();

process.env.REACT_APP_LOADING_TIMEOUT = originalAppLoadingTimeout;
});
});

describe("under error conditions", () => {
const testCases = [true, false];

Expand Down Expand Up @@ -113,3 +162,12 @@ const ContextProvider = ({ children, freshDocument }: { freshDocument: boolean }
</DocumentStateContext.Provider>
);
};

const ChildComponentThatLoadsData = () => {
const { startLoading, finishLoading } = useContext(LoadingContext);

return (<>
<button onClick={startLoading}>START LOADING</button>
<button onClick={finishLoading}>STOP LOADING</button>
</>);
};
32 changes: 27 additions & 5 deletions src/pages/Page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { useContext, useEffect } from "react";
import React, { useCallback, useContext, useEffect, useRef, useState } from "react";
import { ChildrenProps } from "../utilities/children-props";
import { DocumentStateContext } from "../contexts/DocumentStateContext";
import { useFocusHashOrMainHeading } from "../hooks/focus/useFocusHashOrMainHeading";
import "./Pages.css";
import { HeadingCheck } from "../components/dev-only/HeadingCheck";
import { LoadingContextProvider } from "../contexts/LoadingContext";

type PageProps = {
title: string
Expand All @@ -13,6 +14,20 @@ export const DOCUMENT_TITLE_SUFFIX = " - Washtenaw ID Project";

export const Page = ({ title, children }: PageProps) => {
const { documentHasBeenLoaded } = useContext(DocumentStateContext);
const [loadingIndicatorNeeded, setLoadingIndicatorNeeded] = useState(false);
const loadingTimerRef: React.MutableRefObject<NodeJS.Timeout | undefined> = useRef();

const setIsLoading = useCallback((value: boolean) => {
if (value) {
loadingTimerRef.current = setTimeout(() => {
setLoadingIndicatorNeeded(true);
}, parseInt(process.env.REACT_APP_LOADING_TIMEOUT!));
} else {
if (loadingTimerRef.current) {
clearTimeout(loadingTimerRef.current);
}
}
}, []);

useFocusHashOrMainHeading();

Expand All @@ -26,9 +41,16 @@ export const Page = ({ title, children }: PageProps) => {

return (<>
<HeadingCheck/>
<div className={"page-container"}>
{children}
<div aria-live={"polite"} data-testid={"page-live-region"}/>
</div>
<LoadingContextProvider setIsLoading={setIsLoading}>
<div className={"page-container"}>
{loadingIndicatorNeeded
? "Loading..."
: children
}
<div aria-live={"polite"}
data-testid={"page-live-region"}>{loadingIndicatorNeeded ? "loading" : ""}
</div>
</div>
</LoadingContextProvider>
</>);
};
3 changes: 2 additions & 1 deletion src/setupTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { toHaveNoViolations } from "jest-axe";

process.env.REACT_APP_API = "test";
process.env.REACT_APP_FOCUS_TIMEOUT = "10";
process.env.REACT_APP_LOADING_TIMEOUT = "10";

setupTestingServer();

Expand All @@ -18,4 +19,4 @@ global.matchMedia = global.matchMedia || function () {
};
};

expect.extend(toHaveNoViolations);
expect.extend(toHaveNoViolations);

0 comments on commit 811f0a5

Please sign in to comment.