Sunday 28 March 2021

Software Review: Tableau Desktop

During an ongoing foray into the world of Data Analysis, I was introduced to a Data Visualization Tool known as Tableau Desktop. Prior to that, my experience with Data Visualization had been limited to less sophisticated means - via the use of libraries such as D3; or worse, writing code without using libraries.

Tableau takes care of all the scaffolding required to produce passably decent-looking visualizations, and more.


It was picked up by Salesforce in 2019, and this bodes well for its longevity. Today, I'd like to provide a quick overview of what makes Tableau so attractive to a developer; or actually, just about anyone who needs a fuss-free way to analyze data beautifully.

The Premise

Tableau Desktop is used to manipulate one or more datasets, possibly joining them into a coherent whole, then using that data to create charts of several different types. From there, charts can be placed into dashboards, and in turn these dashboards can be used to form a Data Story.

Dashboard and Data Stories

More intricate work such as interactivity can be added via Actions. Some of the cool things you can do using Actions are - redirect to another dashboard, open a URL, change data filters on an existing sheet, and more!


Actions

Data may also be manipulated using built-in functions provided by Tableau.

Built-in functions



The Aesthetics

Tableau's color scheme, for the most part, is a very black and grey on a white background. That works for a clean look. Other colors come in once the charts are being created. By default, it's mostly a muted blue, but this can be customized.

A clean aesthetic.

The Experience

It's fairly intuitive most of the time. Emphasis on "most". For simple charts, it's easy as pie (heh heh). For the more complicated chart types, the process becomes exponentially more complex. Perhaps it's just me, but trial and error reap little reward as opposed to consulting online documentation. Thankfully, the online documentation and tutorials are plentiful.

Response is fast, but in all honesty, the datasets I was working with were fairly limited.

The Interface

The majority of the controls on a drag-and-drop basis, with other functionality available via right-clicks and double-clicks. Options from the main menu duplicate this functionality in case the user prefers that route.

Sometimes there may be a bit of coding, and the interface allows for it. But there is plenty that can still be done without writing a single line of code. Most of the time, code is only involved when you want to do things like custom calculated fields and dashboard interactivity.

Code interface

Additional levels of detail are available in menus upon right-clicking. There's a lot that is customizable.

Many customizations


What I liked

Fast and responsive. I may have mentioned it before, but seeing data sort itself neatly into charts of various shapes without having to do much beyond drag and drop, is a beauty to behold.

Almost effortless. Tableau saves you the trouble of having to painstakingly set up data visualization, by taking care of most of the hard work. All that's really left for the user is to customize the look and feel... and even then it's not a lot of work because the default setting already looks pretty decent.

Huge variety of chart types. There are so many basic chart types, and variations on them. And quite a few I didn't even know existed.

Treemap

Bubble Chart

Dual-axis Chart

Pie Chart

Filled Area Plot

Grouped Bar Chart

Line Chart

Stacked Bar Chart


And while I might not use it much, I'd like to take a moment to gush about geographical data. Tableau has all that covered. If you define data as Geographical, it gives you visual representation on an actual map.

Geographical data

Country names

And if you further break it down into county level, it actually has updated information on that as well! This is what it looks like for Singapore! Awesome, right?!

County names

The different screen layout options for dashboards is a really nice touch. Tableau does have a nose for the essentials.

Layout for screen sizes

Online resources are vast. There will be times when you need help from more experienced users. It's out there, in spades. The official documentation, on its own, is already pretty comprehensive.

What I didn't

Tableau can be complicated. While the interface is pretty intuitive and most basic uses cases can be resolved with relative ease, beyond that, there are times where things get tricky. Putting together something like a stacked bar chart, for example, can be a matter of a ridiculous amount of trial and error. Thankfully, for such scenarios, the extensive online documentation covers it more often than not.

Tableau would really benefit from an "Idiot Mode" - some step-by-step interface where the user chooses the visualization required and the software provides a guide as to what to drag/drop, and where.

Conclusion

There are probably many Data Visualization software packages out there which I haven't had the pleasure of using (unless one counts Microsoft's Excel or Google Charts), so do bear in mind that my frame of reference is woefully limited. However, having had the experience of having to create visualizations without the use of such a tool, my experience with Tableau Desktop has been overwhelmingly positive.

My Rating

8.5 / 10

Give it a go; it'll bleau your mind!
T___T

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