Fix warnings about wrapping tests in act() (#3314)

* Fix warnings about wrapping tests in act()

For full details on what this fix entails, see
https://www.benmvp.com/blog/avoiding-react-act-warning-when-accessibility-testing-next-link-jest-axe/

* fix buttonProps errors

* fix lint

---------

Co-authored-by: Kaitlyn <kandres@mozilla.com>
This commit is contained in:
Vincent 2023-08-29 19:34:23 +02:00 коммит произвёл GitHub
Родитель ed6f4685e0
Коммит e95c3af684
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 70 добавлений и 18 удалений

Просмотреть файл

@ -77,13 +77,13 @@ const customJestConfig = {
statements: 90,
},
"src/app/(proper_react)/redesign/MobileShell.tsx": {
branches: 50,
branches: 25,
functions: 50,
lines: 94,
statements: 94,
},
"src/app/(proper_react)/redesign/PageLink.tsx": {
branches: 60,
branches: 33,
functions: 100,
lines: 100,
statements: 100,
@ -348,7 +348,14 @@ const customJestConfig = {
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
setupFilesAfterEnv: [
"<rootDir>/jest.setup.ts",
// See https://www.benmvp.com/blog/avoiding-react-act-warning-when-accessibility-testing-next-link-jest-axe/
// Mocks the IntersectionObserver API, which is used by Next.js's <Link>.
// This prevents warnings about wrapping tests in act() for components that
// include <Link>s.
"react-intersection-observer/test-utils",
],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,

Просмотреть файл

@ -6,6 +6,7 @@ import "@testing-library/jest-dom";
import { setProjectAnnotations } from "@storybook/react";
import { toHaveNoViolations } from "jest-axe";
import { expect } from "@jest/globals";
import { defaultFallbackInView } from "react-intersection-observer";
import * as globalStorybookConfig from "./.storybook/preview";
@ -14,3 +15,13 @@ setProjectAnnotations(
);
expect.extend(toHaveNoViolations);
// See https://www.benmvp.com/blog/avoiding-react-act-warning-when-accessibility-testing-next-link-jest-axe/
// If no `IntersectionObserver` exists, Next.js's <Link> will do a state update
// immediately after rendering, causing warnings about wrapping tests in act().
global.IntersectionObserver = jest.fn();
// Then in jest.config.cjs, we add an actual mock for the IntersectionObserver
// API in `setupFilesAfterEnv`. When a <Link> scrolls into view, Next.js will
// attempt to preload the target, causing another rerender that would cause a
// warning about wrapping tests in act(). Thus, we tell it it's not in view.
defaultFallbackInView(false);

17
package-lock.json сгенерированный
Просмотреть файл

@ -85,6 +85,7 @@
"node-mocks-http": "^1.12.1",
"nodemon": "^2.0.20",
"prettier": "2.8.8",
"react-intersection-observer": "^9.5.2",
"sass": "^1.62.1",
"storybook": "^7.0.18",
"stylelint": "^15.6.0",
@ -28384,6 +28385,15 @@
"react": "^16.8.4 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-intersection-observer": {
"version": "9.5.2",
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.5.2.tgz",
"integrity": "sha512-EmoV66/yvksJcGa1rdW0nDNc4I1RifDWkT50gXSFnPLYQ4xUptuDD4V7k+Rj1OgVAlww628KLGcxPXFlOkkU/Q==",
"dev": true,
"peerDependencies": {
"react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -53691,6 +53701,13 @@
"dev": true,
"requires": {}
},
"react-intersection-observer": {
"version": "9.5.2",
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.5.2.tgz",
"integrity": "sha512-EmoV66/yvksJcGa1rdW0nDNc4I1RifDWkT50gXSFnPLYQ4xUptuDD4V7k+Rj1OgVAlww628KLGcxPXFlOkkU/Q==",
"dev": true,
"requires": {}
},
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",

Просмотреть файл

@ -121,6 +121,7 @@
"node-mocks-http": "^1.12.1",
"nodemon": "^2.0.20",
"prettier": "2.8.8",
"react-intersection-observer": "^9.5.2",
"sass": "^1.62.1",
"storybook": "^7.0.18",
"stylelint": "^15.6.0",

Просмотреть файл

@ -5,10 +5,10 @@
"use client";
import Image from "next/image";
import { FormEvent, useState } from "react";
import { FormEvent, useRef, useState } from "react";
import { Session } from "next-auth";
import { useOverlayTriggerState } from "react-stately";
import { useOverlayTrigger } from "react-aria";
import { useButton, useOverlayTrigger } from "react-aria";
import whyWeNeedInfoHero from "./images/welcome-why-we-need-info.svg";
import { useL10n } from "../../../../../hooks/l10n";
import { ModalOverlay } from "../../../../../components/client/dialog/ModalOverlay";
@ -166,12 +166,10 @@ export const EnterInfo = ({ onScanStarted, onGoBack }: Props) => {
const handleOnSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const invalidInputKeys = getInvalidFields();
if (!invalidInputKeys?.length) {
confirmDialogState.open();
} else {
if (invalidInputKeys?.length > 0) {
setInvalidInputs(invalidInputKeys);
confirmDialogState.close();
}
};
@ -286,13 +284,20 @@ export const EnterInfo = ({ onScanStarted, onGoBack }: Props) => {
</Dialog>
);
const triggerRef = useRef<HTMLButtonElement>(null);
const { buttonProps } = useButton(
explainerDialogTrigger.triggerProps,
triggerRef
);
return (
<div className={styles.stepContent}>
<h1>{l10n.getString("onboarding-enter-details-title")}</h1>
<p>
{l10n.getString("onboarding-enter-details-text")}
<button
{...explainerDialogTrigger.triggerProps}
{...buttonProps}
ref={triggerRef}
onClick={() => explainerDialogState.open()}
className={styles.explainerTrigger}
>
@ -356,6 +361,7 @@ export const EnterInfo = ({ onScanStarted, onGoBack }: Props) => {
{...confirmDialogTrigger.triggerProps}
variant="primary"
autoFocus={true}
type="submit"
className={styles.startButton}
>
{l10n.getString("onboarding-steps-find-exposures-label")}
@ -373,7 +379,7 @@ export const EnterInfo = ({ onScanStarted, onGoBack }: Props) => {
</ModalOverlay>
)}
{confirmDialogState.isOpen && (
{confirmDialogState.isOpen && getInvalidFields().length === 0 && (
<ModalOverlay
state={confirmDialogState}
{...explainerDialogTrigger.overlayProps}

Просмотреть файл

@ -6,7 +6,7 @@
import Image from "next/image";
import { useOverlayTriggerState } from "react-stately";
import { useOverlayTrigger } from "react-aria";
import { useButton, useOverlayTrigger } from "react-aria";
import howItWorksHero from "./images/welcome-how-it-works.svg";
import { useL10n } from "../../../../../hooks/l10n";
import { ModalOverlay } from "../../../../../components/client/dialog/ModalOverlay";
@ -14,6 +14,7 @@ import { Dialog } from "../../../../../components/client/dialog/Dialog";
import { Button } from "../../../../../components/server/Button";
import styles from "./GetStarted.module.scss";
import { useRef } from "react";
export type Props = {
dataBrokerCount: number;
@ -28,6 +29,12 @@ export const GetStarted = (props: Props) => {
explainerDialogState
);
const triggerRef = useRef<HTMLButtonElement>(null);
const { buttonProps } = useButton(
explainerDialogTrigger.triggerProps,
triggerRef
);
return (
<div className={styles.stepContent}>
<h1>{l10n.getString("onboarding-get-started-heading")}</h1>
@ -35,7 +42,8 @@ export const GetStarted = (props: Props) => {
<p>{l10n.getString("onboarding-get-started-content-price")}</p>
<p>
<button
{...explainerDialogTrigger.triggerProps}
{...buttonProps}
ref={triggerRef}
onClick={() => explainerDialogState.open()}
className={styles.explainerTrigger}
>

Просмотреть файл

@ -2,9 +2,10 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { ComponentProps, HTMLAttributes, ReactNode } from "react";
import { ComponentProps, ReactNode, useRef } from "react";
import Link from "next/link";
import styles from "./button.module.scss";
import { useButton } from "react-aria";
export interface Props extends ComponentProps<"button"> {
children: ReactNode;
@ -14,12 +15,11 @@ export interface Props extends ComponentProps<"button"> {
disabled?: boolean;
href?: string;
isLoading?: boolean;
onClick?: () => void;
small?: boolean;
}
export const Button = (
props: Props & HTMLAttributes<HTMLButtonElement | HTMLLinkElement>
props: Props & Parameters<typeof useButton>[0] // AriaButtonOptions
) => {
const {
buttonType,
@ -28,12 +28,14 @@ export const Button = (
disabled,
href,
isLoading,
onClick,
small,
variant,
...otherProps
} = props;
const buttonRef = useRef<HTMLButtonElement>(null);
const { buttonProps } = useButton(otherProps, buttonRef);
const classes = [
styles.button,
styles[variant],
@ -56,7 +58,7 @@ export const Button = (
{children}
</Link>
) : (
<button {...otherProps} className={classes} onClick={onClick}>
<button {...buttonProps} ref={buttonRef} className={classes}>
{children}
</button>
);