Wednesday 24 March 2021

Web Tutorial: ReactJS Hangman (Part 4/4)

Welcome back!

There's a whole lot of automated interface testing to be done here. There are four components to be tested - App, HangedMan, Computer and Player. While the entire app on its own is workable, we may have to enter some test-id attributes at certain parts to facilitate testing.

Testing App

We first create this file. Some standard testing libraries will need to be imported, as well as the App component. App is pretty straightforward; not that much testing required.

src/App.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import App from "./App";


The main test for this is a rendering test, where the word "Loading" should appear, due to the app loading the API for retrieving the word list.

src/App.test.js

import React from "react";
import { render, screen } from "@testing-library/react";
import App from "./App";

describe("App", () => {
    it('renders', () => {
        render(<App />);
        expect(screen.queryByText('Loading...')).toBeInTheDocument();
    });
});


Testing HangedMan

This one will be a little more complicated. We begin by creating the file which will do the same imports.

src/components/HangedMan/HangedMan.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import HangedMan from "./HangedMan";


Declare stage. This variable will be important because we'll need it to test the ways the hanged man will appear at different stages of the app. Remember how we tested in Part 2 of this web tutorial? Well, this will be an automated version of that.

src/components/HangedMan/HangedMan.test.js

import React from "react";
import { render, screen } from "@testing-library/react";
import HangedMan from "./HangedMan";

let stage;


This will describe the HangedMan suite of tests.
src/components/HangedMan/HangedMan.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import HangedMan from "./HangedMan";

let stage;

describe("HangedMan", () => {

});


Now this first test will test for the value of 0 for stage.

src/components/HangedMan/HangedMan.test.js
describe("HangedMan", () => {
    it("should render with no hanged man parts according to stage 0", () => {
       
    });

});


Here, we set stage to 0 and render HangedMan with that value.

src/components/HangedMan/HangedMan.test.js
describe("HangedMan", () => {
    it("should render with no hanged man parts according to stage 0", () => {
        stage = 0;

        render(
            <HangedMan
                stage={ stage }
            />
        );
       
    });
});


And then we grab all the testing ids from the various SVG tags making up the hanged man. All of them should have hidden as part of their CSS class. Because stage is 0. But this won't work because the testing ids have not been set yet, so...

src/components/HangedMan/HangedMan.test.js
describe("HangedMan", () => {
    it("should render with no hanged man parts according to stage 0", () => {
        stage = 0;

        render(
            <HangedMan
                stage={ stage }
            />
        );

        expect(screen.queryByTestId("hangedMan_head").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("hangedMan_leftArm").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("hangedMan_rightArm").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("hangedMan_torso").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("hangedMan_leftLeg").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("hangedMan_rightLeg").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("txtGameOver").classList.contains("hidden")).toBe(true);   
     
    });
});


...let's add those in.

src/components/HangedMan/HangedMan.js

<g className={ props.stage >= 6 ? 'swing' : '' }>
    <line
        className="rope"
        x1="300"
        y1="20"
        x2="300"
        y2="60"
    ></line>

    <circle
        className={props.stage >= 1 ? 'man' : 'man hidden'}
        cx="300"
        cy="80"
        r="20"
        data-testid="hangedMan_head"
    ></circle>

    <line
        className={props.stage >= 2 ? 'man' : 'man hidden'}
        x1="300"
        y1="100"
        x2="280"
        y2="160"
        data-testid="hangedMan_leftArm"
    ></line>

    <line
        className={props.stage >= 3 ? 'man' : 'man hidden'}
        x1="300"
        y1="100"
        x2="320"
        y2="160"
        data-testid="hangedMan_rightArm"
    ></line>

    <line
        className={props.stage >= 4 ? 'man' : 'man hidden'}
        x1="300"
        y1="100"
        x2="300"
        y2="180"
        data-testid="hangedMan_torso"
    ></line>

    <line
        className={props.stage >= 5 ? 'man' : 'man hidden'}
        x1="300"
        y1="180"
        x2="280"
        y2="250"
        data-testid="hangedMan_leftLeg"
    ></line>

    <line
        className={props.stage >= 6 ? 'man' : 'man hidden'}
        x1="300"
        y1="180"
        x2="320"
        y2="250"
        data-testid="hangedMan_rightLeg"
    ></line>
</g>

<text
    x="350"
    y="100"
    className={props.stage >= 6 ? 'gameover' : 'gameover hidden'}
    data-testid="txtGameOver"
>
    <tspan x="350" y="100">GAME</tspan>
    <tspan x="350" y="145">OVER</tspan>
</text>


The rest of these tests are basically the same - just with a different value for stage. And those elements that have false where expected, will vary according to that value. For instance, if stage is 1, then only the head will not have the class hidden.

src/components/HangedMan/HangedMan.test.js
describe("HangedMan", () => {
    it("should render with no hanged man parts according to stage 0", () => {
        stage = 0;

        render(
            <HangedMan
                stage={ stage }
            />
        );

        expect(screen.queryByTestId("hangedMan_head").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("hangedMan_leftArm").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("hangedMan_rightArm").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("hangedMan_torso").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("hangedMan_leftLeg").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("hangedMan_rightLeg").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("txtGameOver").classList.contains("hidden")).toBe(true);        
    });

    it("should render with only head according to stage 1", () => {
        stage = 1;

        render(
            <HangedMan
                stage={ stage }
            />
        );

        expect(screen.queryByTestId("hangedMan_head").classList.contains("hidden")).toBe(false);
        expect(screen.queryByTestId("hangedMan_leftArm").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("hangedMan_rightArm").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("hangedMan_torso").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("hangedMan_leftLeg").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("hangedMan_rightLeg").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("txtGameOver").classList.contains("hidden")).toBe(true);        
    });

    it("should render with only head and left arm according to stage 2", () => {
        stage = 2;

        render(
            <HangedMan
                stage={ stage }
            />
        );

        expect(screen.queryByTestId("hangedMan_head").classList.contains("hidden")).toBe(false);
        expect(screen.queryByTestId("hangedMan_leftArm").classList.contains("hidden")).toBe(false);
        expect(screen.queryByTestId("hangedMan_rightArm").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("hangedMan_torso").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("hangedMan_leftLeg").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("hangedMan_rightLeg").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("txtGameOver").classList.contains("hidden")).toBe(true);        
    });

    it("should render with only head, left arm and right arm according to stage 3", () => {
        stage = 3;

        render(
            <HangedMan
                stage={ stage }
            />
        );

        expect(screen.queryByTestId("hangedMan_head").classList.contains("hidden")).toBe(false);
        expect(screen.queryByTestId("hangedMan_leftArm").classList.contains("hidden")).toBe(false);
        expect(screen.queryByTestId("hangedMan_rightArm").classList.contains("hidden")).toBe(false);
        expect(screen.queryByTestId("hangedMan_torso").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("hangedMan_leftLeg").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("hangedMan_rightLeg").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("txtGameOver").classList.contains("hidden")).toBe(true);        
    });

    it("should render with only head, left arm, right arm and torso according to stage 4", () => {
        stage = 4;

        render(
            <HangedMan
                stage={ stage }
            />
        );

        expect(screen.queryByTestId("hangedMan_head").classList.contains("hidden")).toBe(false);
        expect(screen.queryByTestId("hangedMan_leftArm").classList.contains("hidden")).toBe(false);
        expect(screen.queryByTestId("hangedMan_rightArm").classList.contains("hidden")).toBe(false);
        expect(screen.queryByTestId("hangedMan_torso").classList.contains("hidden")).toBe(false);
        expect(screen.queryByTestId("hangedMan_leftLeg").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("hangedMan_rightLeg").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("txtGameOver").classList.contains("hidden")).toBe(true);                
    });

    it("should render with only head, left arm, right arm, torso and left leg according to stage 5", () => {
        stage = 5;

        render(
            <HangedMan
                stage={ stage }
            />
        );

        expect(screen.queryByTestId("hangedMan_head").classList.contains("hidden")).toBe(false);
        expect(screen.queryByTestId("hangedMan_leftArm").classList.contains("hidden")).toBe(false);
        expect(screen.queryByTestId("hangedMan_rightArm").classList.contains("hidden")).toBe(false);
        expect(screen.queryByTestId("hangedMan_torso").classList.contains("hidden")).toBe(false);
        expect(screen.queryByTestId("hangedMan_leftLeg").classList.contains("hidden")).toBe(false);
        expect(screen.queryByTestId("hangedMan_rightLeg").classList.contains("hidden")).toBe(true);
        expect(screen.queryByTestId("txtGameOver").classList.contains("hidden")).toBe(true);        
    });

    it("should render with full hanged man and game over text according to stage 6", () => {
        stage = 6;

        render(
            <HangedMan
                stage={ stage }
            />
        );

        expect(screen.queryByTestId("hangedMan_head").classList.contains("hidden")).toBe(false);
        expect(screen.queryByTestId("hangedMan_leftArm").classList.contains("hidden")).toBe(false);
        expect(screen.queryByTestId("hangedMan_rightArm").classList.contains("hidden")).toBe(false);
        expect(screen.queryByTestId("hangedMan_torso").classList.contains("hidden")).toBe(false);
        expect(screen.queryByTestId("hangedMan_leftLeg").classList.contains("hidden")).toBe(false);
        expect(screen.queryByTestId("hangedMan_rightLeg").classList.contains("hidden")).toBe(false);
        expect(screen.queryByTestId("txtGameOver").classList.contains("hidden")).toBe(false);
    });

});


That was actually pretty easy. Let's move on to something slightly more complex.

Testing Computer

Yep you guessed it. Create the file in the appropriate directory and import the files required.

src/components/Computer/Computer.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import Computer from "./Computer";


Declare guessedLetters and mysteryWord. Those are our testing variables.

src/components/Computer/Computer.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import Computer from "./Computer";

let guessedLetters;
let mysteryWord;


Again, we describe the test group for this.

src/components/Computer/Computer.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import Computer from "./Computer";

let guessedLetters;
let mysteryWord;

describe("Computer", () => {

});


There is only one test. And here, we test that the display component shows the letters correctly.

src/components/Computer/Computer.test.js
describe("Computer", () => {
    it("should render with Computer display component when game in progress", () => {

    });

});


So here, we set "friend" as the mystery word, and declare that the letters "i" and "e" have been guessed. Then we render Computer and pass in these values.

src/components/Computer/Computer.test.js

describe("Computer", () => {
    it("should render with Computer display component when game in progress", () => {
        mysteryWord = "friend";
        guessedLetters = ["i", "e"];

        render(
            <Computer
                mysteryWord={ mysteryWord }
                guessedLetters={ guessedLetters }
            />
        );

    });
});


Now we check the screen for all elements containing these exact letters making up "friend". Since "i" and "e" have been guessed, only these two should be visible.

src/components/Computer/Computer.test.js
describe("Computer", () => {
    it("should render with Computer display component when game in progress", () => {
        mysteryWord = "friend";
        guessedLetters = ["i", "e"];

        render(
            <Computer
                mysteryWord={ mysteryWord }
                guessedLetters={ guessedLetters }
            />
        );

        expect(screen.queryByText("f")).not.toBeInTheDocument();
        expect(screen.queryByText("r")).not.toBeInTheDocument();
        expect(screen.queryByText("i")).toBeInTheDocument();
        expect(screen.queryByText("e")).toBeInTheDocument();
        expect(screen.queryByText("n")).not.toBeInTheDocument();
        expect(screen.queryByText("d")).not.toBeInTheDocument();

    });
});


Testing Player

Now this is a big one. That's where all the user input is.

Create the file and do the imports. In this case, we also import userEvent because we'll be testing using simulated button clicks and text inputs.

src/components/Player/Player.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Player from "./Player";


Declare error, isPending and mysteryWord. Those are testing variables.

src/components/Player/Player.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Player from "./Player";

let error;
let isPending;
let mysteryWord;


Also declare guessedLetters and create a simple mutator, setGuessedLetters(). Do the same for stage and setStage(). Declare setMessageAndContext() but we won't bother to put anything in it because we won't be testing that output. In fact, we won't be using setMessageAndContext() except to pass it down to the Player component in props.

src/components/Player/Player.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Player from "./Player";

let guessedLetters;
let setGuessedLetters = (arr)=> {
    guessedLetters = arr;
};

let stage;
let setStage = (num)=> {
    stage = num;
};

let setMessageAndContext = (x)=> {

};


let error;
let isPending;
let mysteryWord;


We also set window.alert as a kind of dummy placeholder so that if it's ever called by any of our simulated events, no errors will be thrown. Remember one of our event handlers runs an alert() function?

src/components/Player/Player.test.js
let error;
let isPending;
let mysteryWord;

window.alert = ()=> {};


And finally, we begin the test suite. There are going to be quite a few tests.

src/components/Player/Player.test.js
let error;
let isPending;
let mysteryWord;

window.alert = ()=> {};

describe("Player", () => {

});


The first test ensures that the correct button is shown when the game has not started. So we set error to undefined, isPending to false, set mysteryWord to a random word and stage is set to -1. We can set stage directly instead of using a mutator, but since we already went to the trouble of defining the mutator, what the heck, right?

src/components/Player/Player.test.js
describe("Player", () => {
    it("should render with Begin button when game not started", () => {
        error = undefined;
        isPending = false;
        mysteryWord = "evergreen";
        setStage(-1);
    });

});


Then render Player with the appropriate arguments.

src/components/Player/Player.test.js
describe("Player", () => {
    it("should render with Begin button when game not started", () => {
        error = undefined;
        isPending = false;
        mysteryWord = "evergreen";
        setStage(-1);

        render(
            <Player
                stage={ stage }
                setStage={ setStage }
                mysteryWord={ mysteryWord }
                guessedLetters={ guessedLetters }
                setGuessedLetters={ setGuessedLetters }
                setMessageAndContext={ setMessageAndContext }
                error={ error }
                isPending={ isPending }
            />
        );

    });
});


After that, we should check if there is any element with "Begin" is in the document.

src/components/Player/Player.test.js
describe("Player", () => {
    it("should render with Begin button when game not started", () => {
        error = undefined;
        isPending = false;
        mysteryWord = "evergreen";
        setStage(-1);

        render(
            <Player
                stage={ stage }
                setStage={ setStage }
                mysteryWord={ mysteryWord }
                guessedLetters={ guessedLetters }
                setGuessedLetters={ setGuessedLetters }
                setMessageAndContext={ setMessageAndContext }
                error={ error }
                isPending={ isPending }
            />
        );

        expect(screen.queryByText("Begin")).toBeInTheDocument();
    });
});


Similar logic is deployed for the "Replay" button. This time, stage is set to 6.
src/components/Player/Player.test.js
describe("Player", () => {
    it("should render with Begin button when game not started", () => {
        error = undefined;
        isPending = false;
        mysteryWord = "evergreen";
        setStage(-1);

        render(
            <Player
                stage={ stage }
                setStage={ setStage }
                mysteryWord={ mysteryWord }
                guessedLetters={ guessedLetters }
                setGuessedLetters={ setGuessedLetters }
                setMessageAndContext={ setMessageAndContext }
                error={ error }
                isPending={ isPending }
            />
        );

        expect(screen.queryByText("Begin")).toBeInTheDocument();
    });

    it("should render with Replay button when game over", () => {
        error = undefined;
        isPending = false;
        mysteryWord = "evergreen";
        setStage(6);

        render(
            <Player
                stage={ stage }
                setStage={ setStage }
                mysteryWord={ mysteryWord }
                guessedLetters={ guessedLetters }
                setGuessedLetters={ setGuessedLetters }
                setMessageAndContext={ setMessageAndContext }
                error={ error }
                isPending={ isPending }
            />
        );

        expect(screen.queryByText("Replay")).toBeInTheDocument();
    });

});


The next test is for when the game is in progress, so set stage to any value between 0 and 4 inclusive. And render Player.

src/components/Player/Player.test.js
it("should render with Replay button when game over", () => {
    error = undefined;
    isPending = false;
    mysteryWord = "evergreen";
    setStage(6);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );

    expect(screen.queryByText("Replay")).toBeInTheDocument();
});

it("should render with Player dashboard component when game in progress", () => {
    error = undefined;
    isPending = false;
    mysteryWord = "evergreen";
    setStage(3);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );
});


We should test for the presence of "Select A Letter", "Guess The Word" and all letters of the alphabet. Seems like overkill, but yeah, let's do that.

src/components/Player/Player.test.js
it("should render with Replay button when game over", () => {
    error = undefined;
    isPending = false;
    mysteryWord = "evergreen";
    setStage(6);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );

    expect(screen.queryByText("Replay")).toBeInTheDocument();
});

it("should render with Player dashboard component when game in progress", () => {
    error = undefined;
    isPending = false;
    mysteryWord = "evergreen";
    setStage(3);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );

    expect(screen.queryByText("Select A Letter")).toBeInTheDocument();
    expect(screen.queryByText("Guess The Word")).toBeInTheDocument();
    expect(screen.queryByText("a")).toBeInTheDocument();
    expect(screen.queryByText("b")).toBeInTheDocument();
    expect(screen.queryByText("c")).toBeInTheDocument();
    expect(screen.queryByText("d")).toBeInTheDocument();
    expect(screen.queryByText("e")).toBeInTheDocument();
    expect(screen.queryByText("f")).toBeInTheDocument();
    expect(screen.queryByText("g")).toBeInTheDocument();
    expect(screen.queryByText("h")).toBeInTheDocument();
    expect(screen.queryByText("i")).toBeInTheDocument();
    expect(screen.queryByText("j")).toBeInTheDocument();
    expect(screen.queryByText("k")).toBeInTheDocument();
    expect(screen.queryByText("l")).toBeInTheDocument();
    expect(screen.queryByText("m")).toBeInTheDocument();
    expect(screen.queryByText("n")).toBeInTheDocument();
    expect(screen.queryByText("o")).toBeInTheDocument();
    expect(screen.queryByText("p")).toBeInTheDocument();
    expect(screen.queryByText("q")).toBeInTheDocument();
    expect(screen.queryByText("r")).toBeInTheDocument();
    expect(screen.queryByText("s")).toBeInTheDocument();
    expect(screen.queryByText("t")).toBeInTheDocument();
    expect(screen.queryByText("u")).toBeInTheDocument();
    expect(screen.queryByText("v")).toBeInTheDocument();
    expect(screen.queryByText("w")).toBeInTheDocument();
    expect(screen.queryByText("x")).toBeInTheDocument();
    expect(screen.queryByText("y")).toBeInTheDocument();
    expect(screen.queryByText("z")).toBeInTheDocument();

});


Next thing to do is to ensure that if error is true, clicking on "Begin" does nothing. So set error to an Error object, set the other variables, and render Player.

src/components/Player/Player.test.js
it("should render with Player dashboard component when game in progress", () => {
    error = undefined;
    isPending = false;
    mysteryWord = "evergreen";
    setStage(3);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );

    expect(screen.queryByText("Select A Letter")).toBeInTheDocument();
    expect(screen.queryByText("Guess The Word")).toBeInTheDocument();
    expect(screen.queryByText("a")).toBeInTheDocument();
    expect(screen.queryByText("b")).toBeInTheDocument();
    expect(screen.queryByText("c")).toBeInTheDocument();
    expect(screen.queryByText("d")).toBeInTheDocument();
    expect(screen.queryByText("e")).toBeInTheDocument();
    expect(screen.queryByText("f")).toBeInTheDocument();
    expect(screen.queryByText("g")).toBeInTheDocument();
    expect(screen.queryByText("h")).toBeInTheDocument();
    expect(screen.queryByText("i")).toBeInTheDocument();
    expect(screen.queryByText("j")).toBeInTheDocument();
    expect(screen.queryByText("k")).toBeInTheDocument();
    expect(screen.queryByText("l")).toBeInTheDocument();
    expect(screen.queryByText("m")).toBeInTheDocument();
    expect(screen.queryByText("n")).toBeInTheDocument();
    expect(screen.queryByText("o")).toBeInTheDocument();
    expect(screen.queryByText("p")).toBeInTheDocument();
    expect(screen.queryByText("q")).toBeInTheDocument();
    expect(screen.queryByText("r")).toBeInTheDocument();
    expect(screen.queryByText("s")).toBeInTheDocument();
    expect(screen.queryByText("t")).toBeInTheDocument();
    expect(screen.queryByText("u")).toBeInTheDocument();
    expect(screen.queryByText("v")).toBeInTheDocument();
    expect(screen.queryByText("w")).toBeInTheDocument();
    expect(screen.queryByText("x")).toBeInTheDocument();
    expect(screen.queryByText("y")).toBeInTheDocument();
    expect(screen.queryByText("z")).toBeInTheDocument();
});

it("should not react to button clicks if error", () => {
    error = new Error("");
    isPending = false;
    mysteryWord = "evergreen";
    setStage(-1);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );
});


Then simulate an event by using the userEvent object and the click() method on the "Begin" button. "Begin" should still be in the document after that.

src/components/Player/Player.test.js
it("should not react to button clicks if error", () => {
    error = new Error("");
    isPending = false;
    mysteryWord = "evergreen";
    setStage(-1);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );

    userEvent.click(screen.getByText("Begin"));
    expect(screen.queryByText("Begin")).toBeInTheDocument();

});


Similar logic if pending is true.

src/components/Player/Player.test.js
it("should not react to button clicks if error", () => {
    error = new Error("");
    isPending = false;
    mysteryWord = "evergreen";
    setStage(-1);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );

    userEvent.click(screen.getByText("Begin"));
    expect(screen.queryByText("Begin")).toBeInTheDocument();
});

it("should not react to button clicks if pending", () => {
    error = undefined;
    isPending = true;
    mysteryWord = "evergreen";
    setStage(-1);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );

    userEvent.click(screen.getByText("Begin"));
    expect(screen.queryByText("Begin")).toBeInTheDocument();
});


Next, we test for correct input. This is to ensure only valid characters are entered. Set the variables to ensure game is in progress, and render Player.

src/components/Player/Player.test.js
it("should not react to button clicks if pending", () => {
    error = undefined;
    isPending = true;
    mysteryWord = "evergreen";
    setStage(-1);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );

    userEvent.click(screen.getByText("Begin"));
    expect(screen.queryByText("Begin")).toBeInTheDocument();
});

it("should only allow letters to be input when guessing word", () => {
    error = undefined;
    isPending = false;
    mysteryWord = "evergreen";
    setStage(3);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );
});


Then simulate tying into the input by using the type() method of the userEvent object. Enter something obviously invalid, like a number. And in the next line, ensure that the value in the text box is that very string we used, but with only alphabetical characters remaining!

src/components/Player/Player.test.js
it("should only allow letters to be input when guessing word", () => {
    error = undefined;
    isPending = false;
    mysteryWord = "evergreen";
    setStage(3);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );
            
    userEvent.type(screen.getByTestId("txtGuessWord"), "xyz123-abc");

    expect(screen.getByTestId("txtGuessWord").value).toBe("xyzabc");

});


OK, but there's no component known as txtGuessWord. We'll need to insert the testing id here before this will test correctly.

src/components/Player/Player.js
<input
    type="text"
    maxLength="13"
    value={ guessedWord }
    onChange={ (e)=>{ setGuessedWord(RemoveIllegalCharacters(e.target.value)); }}
    data-testid="txtGuessWord"
/>


And now... we will test the game by correctly "guessing" the word.

src/components/Player/Player.test.js
it("should only allow letters to be input when guessing word", () => {
    error = undefined;
    isPending = false;
    mysteryWord = "evergreen";
    setStage(3);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );
            
    userEvent.type(screen.getByTestId("txtGuessWord"), "xyz123-abc");

    expect(screen.getByTestId("txtGuessWord").value).toBe("xyzabc");
});

it("should end game if guessed word is correct", () => {

});


Here, we repeat what we did for the last test. Note that stage is 3, which means that the game is in progress.

src/components/Player/Player.test.js
it("should end game if guessed word is correct", () => {
    error = undefined;
    isPending = false;
    mysteryWord = "evergreen";
    setStage(3);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    ); 
           
});


Then simulate input with text "evergreen", and simulate a click on the Confirm button. Since the input matches mysteryWord, stage should now be -1.

src/components/Player/Player.test.js
it("should end game if guessed word is correct", () => {
    error = undefined;
    isPending = false;
    mysteryWord = "evergreen";
    setStage(3);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );
            
    userEvent.type(screen.getByTestId("txtGuessWord"), "evergreen");
    userEvent.click(screen.getByText("Confirm"));

    expect(stage).toBe(-1);

});


The next test is similar, expect that we deliberately test with an incorrect value. Instead of being set to -1, stage should be incremented by 1.

src/components/Player/Player.test.js
it("should end game if guessed word is correct", () => {
    error = undefined;
    isPending = false;
    mysteryWord = "evergreen";
    setStage(3);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );
            
    userEvent.type(screen.getByTestId("txtGuessWord"), "evergreen");
    userEvent.click(screen.getByText("Confirm"));

    expect(stage).toBe(-1);
});

it("should increment stage if guessed word is incorrect", () => {
    error = undefined;
    isPending = false;
    mysteryWord = "evergreen";
    setStage(3);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );
            
    userEvent.type(screen.getByTestId("txtGuessWord"), "notevergreen");
    userEvent.click(screen.getByText("Confirm"));

    expect(stage).toBe(4);
});


Next test is for letter input.

src/components/Player/Player.test.js
it("should increment stage if guessed word is incorrect", () => {
    error = undefined;
    isPending = false;
    mysteryWord = "evergreen";
    setStage(3);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );
            
    userEvent.type(screen.getByTestId("txtGuessWord"), "notevergreen");
    userEvent.click(screen.getByText("Confirm"));

    expect(stage).toBe(4);
});

it("should handle if guessed letter is correct", () => {

});


The testing variables are the same, but here we will include guessedLetters and ensure it is an empty array.

src/components/Player/Player.test.js
it("should handle if guessed letter is correct", () => {
    error = undefined;
    isPending = false;
    mysteryWord = "evergreen";
    guessedLetters = [];
    setStage(3);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );

});


Simulate a click on the letter "e". stage should still be 3, and there should be an element inserted into guessedLetters.

src/components/Player/Player.test.js
it("should handle if guessed letter is correct", () => {
    error = undefined;
    isPending = false;
    mysteryWord = "evergreen";
    guessedLetters = [];
    setStage(3);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );
            
    userEvent.click(screen.getByTestId("btnLetter_e"));
    
    expect(stage).toBe(3);
    expect(guessedLetters.length).toBe(1);

});


This test, of cours, will be useless if we don't insert testing ids...
src/components/Player/Player.js
const keyboard = playerLetters.map((item, index) => (
    <div
        key={'letter_' + index}
        className={usedLetters.indexOf(item) === -1 ? 'Key' : 'Key hidden'}
        onClick={()=>{LetterClick(item);}}
        data-testid={'btnLetter_' + item}
    >
        {item}                  
    </div>
    )
);


This next test is for incorrect letter input. The letter "y" is not in "evergreen", so state is incremented to 4, and guessedLetters is still an empty array.

src/components/Player/Player.test.js
it("should handle if guessed letter is correct", () => {
    error = undefined;
    isPending = false;
    mysteryWord = "evergreen";
    guessedLetters = [];
    setStage(3);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );
            
    userEvent.click(screen.getByTestId("btnLetter_e"));
    
    expect(stage).toBe(3);
    expect(guessedLetters.length).toBe(1);
});

it("should handle if guessed letter is incorrect", () => {
    error = undefined;
    isPending = false;
    mysteryWord = "evergreen";
    guessedLetters = [];
    setStage(3);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );
            
    userEvent.click(screen.getByTestId("btnLetter_y"));

    expect(stage).toBe(4);
    expect(guessedLetters.length).toBe(0);
});


Now let's take the test for correct input further.

src/components/Player/Player.test.js
it("should handle if guessed letter is incorrect", () => {
    error = undefined;
    isPending = false;
    mysteryWord = "evergreen";
    guessedLetters = [];
    setStage(3);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );
            
    userEvent.click(screen.getByTestId("btnLetter_y"));

    expect(stage).toBe(4);
    expect(guessedLetters.length).toBe(0);
});

it("should handle if guessed letters win the game", () => {

});


Here, we repeat what we did what we did for guessing correct letters.

src/components/Player/Player.test.js
it("should handle if guessed letters win the game", () => {
    error = undefined;
    isPending = false;
    mysteryWord = "evergreen";
    guessedLetters = [];
    setStage(3);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );

});


We simulate clicks on all the letters within the word "evergreen". stage should now be set to -1.

src/components/Player/Player.test.js
it("should handle if guessed letters win the game", () => {
    error = undefined;
    isPending = false;
    mysteryWord = "evergreen";
    guessedLetters = [];
    setStage(3);

    render(
        <Player
            stage={ stage }
            setStage={ setStage }
            mysteryWord={ mysteryWord }
            guessedLetters={ guessedLetters }
            setGuessedLetters={ setGuessedLetters }
            setMessageAndContext={ setMessageAndContext }
            error={ error }
            isPending={ isPending }
        />
    );
            
    userEvent.click(screen.getByTestId("btnLetter_e"));
    userEvent.click(screen.getByTestId("btnLetter_v"));
    userEvent.click(screen.getByTestId("btnLetter_r"));
    userEvent.click(screen.getByTestId("btnLetter_g"));
    userEvent.click(screen.getByTestId("btnLetter_n"));
    
    expect(stage).toBe(-1);

});


And when you finally run the tests, you should see this.


We're done!

That was a whole lot of testing, certainly. Hopefully, you see what we did mostly was automate tests that we were already carrying out during the game. Certainly the cverage of this testing can be increased if you can think of more tests.

This web tutorial was possible only from the tutelege I received from Red Airship. In fact, it was the first app I wrote using what I had learned from the experience. It was fun to actually build something, no matter how insignificant. Hope you had fun too!

Hang in there,
T___T

No comments:

Post a Comment