Search code examples
javascriptreactjsreact-testing-librarytesting-library

Why does query fail in separate `it` blocks of tests


I have a React SignUp component. I'm writing tests with Jest as a runner and testing-library to write tests.

The signup component code( Using materialize for styling) is :

import React, { useState } from "react";
import Card from "@material-ui/core/Card";
import CardActions from "@material-ui/core/CardActions";
import CardContent from "@material-ui/core/CardContent";
import Button from "@material-ui/core/Button";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import Icon from "@material-ui/core/Icon";
import { makeStyles } from "@material-ui/core/styles";
import { create } from "./api-user.js";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogTitle from "@material-ui/core/DialogTitle";
import { Link } from "react-router-dom";

const useStyles = makeStyles((theme) => ({
  card: {
    maxWidth: 600,
    margin: "auto",
    textAlign: "center",
    marginTop: theme.spacing(5),
    paddingBottom: theme.spacing(2),
  },
  error: {
    verticalAlign: "middle",
  },
  title: {
    marginTop: theme.spacing(2),
    color: theme.palette.openTitle,
  },
  textField: {
    marginLeft: theme.spacing(1),
    marginRight: theme.spacing(1),
    width: 300,
  },
  submit: {
    margin: "auto",
    marginBottom: theme.spacing(2),
  },
}));

export default function Signup() {
  const classes = useStyles();
  const [values, setValues] = useState({
    name: "",
    password: "",
    email: "",
    open: false,
    error: "",
  });

  const handleChange = (name) => (event) => {
    setValues({ ...values, [name]: event.target.value });
  };

  const clickSubmit = () => {
    const user = {
      name: values.name || undefined,
      email: values.email || undefined,
      password: values.password || undefined,
    };
    create(user).then((data) => {
      if (data.error) {
        setValues({ ...values, error: data.error });
      } else {
        setValues({ ...values, error: "", open: true });
      }
    });
  };
  return (
    <div>
      <Card className={classes.card}>
        <CardContent>
          <Typography variant="h6" className={classes.title}>
            Sign Up
          </Typography>
          <TextField
            role="name"
            id="name"
            label="Name"
            className={classes.textField}
            value={values.name}
            onChange={handleChange("name")}
            margin="normal"
          />
          <br />
          <TextField
            role="email"
            id="email"
            type="email"
            label="Email"
            className={classes.textField}
            value={values.email}
            onChange={handleChange("email")}
            margin="normal"
          />
          <br />
          <TextField
            role="password"
            id="password"
            type="password"
            label="Password"
            className={classes.textField}
            value={values.password}
            onChange={handleChange("password")}
            margin="normal"
          />
          <br />{" "}
          {values.error && (
            <Typography component="p" color="error">
              <Icon color="error" className={classes.error}>
                error
              </Icon>
              {values.error}
            </Typography>
          )}
        </CardContent>
        <CardActions>
          <Button
            color="primary"
            variant="contained"
            onClick={clickSubmit}
            className={classes.submit}
          >
            Submit
          </Button>
        </CardActions>
      </Card>
      <Dialog open={values.open} disableBackdropClick={true}>
        <DialogTitle>New Account</DialogTitle>
        <DialogContent>
          <DialogContentText>
            New account successfully created.
          </DialogContentText>
        </DialogContent>
        <DialogActions>
          <Link to="/signin">
            <Button color="primary" autoFocus="autoFocus" variant="contained">
              Sign In
            </Button>
          </Link>
        </DialogActions>
      </Dialog>
    </div>
  );
}

I'm trying to render the component and then verifying that the form has Name, Email, and Password inputs. The test file looks like this :

import React from "react";
import { screen, render, cleanup, fireEvent } from "@testing-library/react";
import SignUp from "./Signup";

describe("App component", () => {
  beforeAll(() => {
    render(<SignUp />);
  });
  it("should have the input field for name", () => {
    const nameInput = screen.getByLabelText("Name");
    expect(nameInput).toBeInTheDocument();
  });
  it("should have the input field for email", () => {
    const emailInput = screen.getByLabelText("Email");
    expect(emailInput).toBeInTheDocument();
  });
  it("should have the input field for password", () => {
    const passwordInput = screen.getByLabelText("Password");
    expect(passwordInput).toBeInTheDocument();
  });
  afterAll(cleanup);
});

The Problem: The first case/test always passes irrespective of what order I test the inputs. The .getByLabelText query is able to find the component for the first it block and fails for the next 2.

The message for running the test for the file above is :

 FAIL  client/user/SignUp.test.js
  App component
    ✓ should have the input field for name (13 ms)
    ✕ should have the input field for email (3 ms)
    ✕ should have the input field for password (1 ms)

  ● App component › should have the input field for email

    TestingLibraryElementError: Unable to find a label with the text of: Email

    Ignored nodes: comments, script, style
    <body />

      12 |   });
      13 |   it("should have the input field for email", () => {
    > 14 |     const emailInput = screen.getByLabelText("Email");
         |                               ^
      15 |     expect(emailInput).toBeInTheDocument();
      16 |   });
      17 |   it("should have the input field for password", () => {

      at Object.getElementError (node_modules/@testing-library/dom/dist/config.js:40:19)
      at getAllByLabelText (node_modules/@testing-library/dom/dist/queries/label-text.js:116:38)
      at node_modules/@testing-library/dom/dist/query-helpers.js:62:17
      at node_modules/@testing-library/dom/dist/query-helpers.js:111:19
      at Object.getByLabelText (client/user/SignUp.test.js:14:31)

  ● App component › should have the input field for password

and when I switch the order to :

 it("should have the input field for email", () => {
    const emailInput = screen.getByLabelText("Email");
    expect(emailInput).toBeInTheDocument();
  });
  it("should have the input field for name", () => {
    const nameInput = screen.getByLabelText("Name");
    expect(nameInput).toBeInTheDocument();
  });
  it("should have the input field for password", () => {
    const passwordInput = screen.getByLabelText("Password");
    expect(passwordInput).toBeInTheDocument();
  });

The message changes to:

FAIL  client/user/SignUp.test.js
  App component
    ✓ should have the input field for email (19 ms)
    ✕ should have the input field for name (8 ms)
    ✕ should have the input field for password (6 ms)

  ● App component › should have the input field for name

    TestingLibraryElementError: Unable to find a label with the text of: Name

    Ignored nodes: comments, script, style
    <body />

      13 |   });
      14 |   it("should have the input field for name", () => {
    > 15 |     const nameInput = screen.getByLabelText("Name");
         |                              ^
      16 |     expect(nameInput).toBeInTheDocument();
      17 |   });
      18 |   it("should have the input field for password", () => {

      at Object.getElementError (node_modules/@testing-library/dom/dist/config.js:40:19)
      at getAllByLabelText (node_modules/@testing-library/dom/dist/queries/label-text.js:116:38)
      at node_modules/@testing-library/dom/dist/query-helpers.js:62:17
      at node_modules/@testing-library/dom/dist/query-helpers.js:111:19
      at Object.getByLabelText (client/user/SignUp.test.js:15:30)

  ● App component › should have the input field for password

    TestingLibraryElementError: Unable to find a label with the text of: Password

    Ignored nodes: comments, script, style
    <body />

      17 |   });
      18 |   it("should have the input field for password", () => {
    > 19 |     const passwordInput = screen.getByLabelText("Password");
         |                                  ^
      20 |     expect(passwordInput).toBeInTheDocument();
      21 |   });
      22 |   afterAll(cleanup);

      at Object.getElementError (node_modules/@testing-library/dom/dist/config.js:40:19)
      at getAllByLabelText (node_modules/@testing-library/dom/dist/queries/label-text.js:116:38)
      at node_modules/@testing-library/dom/dist/query-helpers.js:62:17
      at node_modules/@testing-library/dom/dist/query-helpers.js:111:19
      at Object.getByLabelText (client/user/SignUp.test.js:19:34) 

As you can see now the query for email works versus the other 2(name and email)

What's funnier, is that when I use all 3 queries inside a single it block then all test passes:

it("should have the input field for name", () => {
    const nameInput = screen.getByLabelText("Name");
    expect(nameInput).toBeInTheDocument();
    const emailInput = screen.getByLabelText("Email");
    expect(emailInput).toBeInTheDocument();
    const passwordInput = screen.getByLabelText("Password");
    expect(passwordInput).toBeInTheDocument();
  }); 

and it validates all 3 queries, you can test(SodeSandBox: https://codesandbox.io/s/react-testing-library-material-ui-select-forked-5rixw3?file=/src/App.test.js:254-891) it by changing the parameter to the queries, it will fail if you changing it to like const emailInput = screen.getByLabelText("Email-failing");

Can someone tell me, why the test library works in the same block and not in separate blocks? and how can I test three inputs? I have also tried different query methods e.g .getByRole for the role attribute and it behaves in the same fashion i.e works in a single block and 2nd and 3rd blocks/tests fail.


Solution

  • You are rendering your component before all the tests. You should instead render component in each test. Don't use render in beforeAll block, use it inside each 'it' block.

    import React from "react";
    import { screen, render, cleanup, fireEvent } from "@testing- 
    library/react";
    import SignUp from "./Signup";
    
    describe("App component", () => {
      it("should have the input field for name", () => {
        render(<SignUp />);
        const nameInput = screen.getByLabelText("Name");
        expect(nameInput).toBeInTheDocument();
      });
      it("should have the input field for email", () => {
        render(<SignUp />);
        const emailInput = screen.getByLabelText("Email");
        expect(emailInput).toBeInTheDocument();
      });
      it("should have the input field for password", () => {
        render(<SignUp />);
        const passwordInput = screen.getByLabelText("Password");
        expect(passwordInput).toBeInTheDocument();
      });
      afterAll(cleanup);
    });