Tuesday 22 December 2020

Web Tutorial: The Twelve Days of Christmas (Part 2/2)

I'm back!

What we're missing here are images. Let's not waste those beautiful SVG files! Import the SVGs from the svg folder.

src/components/Row/Row.js
import React from 'react';
import day1 from '../../svg/day1.svg';
import day2 from '../../svg/day2.svg';
import day3 from '../../svg/day3.svg';
import day4 from '../../svg/day4.svg';
import day5 from '../../svg/day5.svg';
import day6 from '../../svg/day6.svg';
import day7 from '../../svg/day7.svg';
import day8 from '../../svg/day8.svg';
import day9 from '../../svg/day9.svg';
import day10 from '../../svg/day10.svg';
import day11 from '../../svg/day11.svg';
import day12 from '../../svg/day12.svg';

import './Row.css';


Then modify the content array. Each object should now have a file property, corresponding to the day number... except for the element at index 0 because that's a dummy, remember?

src/components/Row/Row.js

let content = [
    { text: '', file: '' },
    { text: 'a partridge in a pear tree!', file: day1 },
    { text: 'two turtle doves, and', file: day2 },
    { text: 'three french hens', file: day3 },
    { text: 'four calling birds', file: day4 },
    { text: 'five gold rings', file: day5 },
    { text: 'six swans a-swimming', file: day6 },
    { text: 'seven geese a-laying', file: day7 },
    { text: 'eight maids a-milking', file: day8 },
    { text: 'nine ladies dancing', file: day9 },
    { text: 'ten lords a-leaping', file: day10 },
    { text: 'eleven pipers piping', file: day11 },
    { text: 'twelve drummers drumming', file: day12 }
];


We could now do this, and it would totally work. Just observe...

src/components/Row/Row.js
return (
    <div
        className={ 'Row' + (currentDay >= day ? '' : ' Hidden') }
    >
        <h1>{ content[day].text }</h1>
        <img src={ content[day].file } width="100" height="100" />
    </div>
);




But that's just not good enough. What we want is for n number of images for each day. So if it's Day Three, we want three french hens!

Declare an array, arr. Using a For loop and the push() method, make it have day elements. The elements can be empty; we just want to use it in a map() method.

src/components/Row/Row.js
let arr = [];
for (let i = 0; i < day; i++) arr.push('');


return (
    <div
        className={ 'Row' + (currentDay >= day ? '' : ' Hidden') }
    >


Then declare the component images. Use the map() method to add the required number of appropriate images!

src/components/Row/Row.js
let arr = [];
for (let i = 0; i < day; i++) arr.push('');

const images = arr.map((item, index) => (
        <img key={ day+'img'+index } src={ content[day].file } width="100" height="100" />
    )
);


return (
    <div
        className={ 'Row' + (currentDay >= day ? '' : ' Hidden') }
    >


Then do this.
src/components/Row/Row.js
return (
    <div
        className={ 'Row' + (currentDay >= day ? '' : ' Hidden') }
    >
        <h1>{ content[day].text }</h1>
        { images }
    </div>
);


Magic!


That's it!

Functionality-wise, we're good to go. But if you're interested in learning how to test the app, read on.

Testing

Testing for ReactJS apps isn't really Unit Testing, per se. It's more like DOM behavioral testing, if that makes sense. Let's work on the App component. We will use the ReactJS testing library. Basically, you write the tests and use the command npm test.

In this file, import React and the render and screen objects, and the userEvent object from the testing library. Also import App because that's what we're testing.

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


We begin by describing App. This will contain the suite of App's tests.

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

describe("App", () => {

});


First, we want to test that App responds to the up and down buttons. So we add a descriptor, and render App.

src/App/App.test.js
describe("App", () => {
    it("should react to up/down button", () => {
        render(
            <App/>
        );
    });

});


What we need next,is a way for the testing library to be able to identify the buttons. So in the App component, add the data-testid attribute for the buttons and give them values.

src/App/App.js
<div className="dayButtons">
    <div className="dayButton" data-testid="BtnUp" onClick={ BtnClickUp }>&#9650;</div>
    <div className="dayButton" data-testid="BtnDown" onClick={ BtnClickDown }>&#9660;</div>
</div>


Also add data-testid for the span tag that encapsulates currentDay.
src/App/App.js
<span data-testid="lblCurrentDay">
{ currentDay }
</span>


By default, day is 1. So if BtnUp is clicked, lblCurrentDay's content should be "2".
src/App/App.test.js
it("should react to up/down button", () => {
    render(
        <App/>
    );

    userEvent.click(screen.getByTestId("BtnUp"));
    expect(screen.getByTestId("lblCurrentDay").textContent).toBe("2");

});


And after that, if BtnDown is clicked, the value should revert to "1". That's how we test using userEvent.

src/App/App.test.js
it("should react to up/down button", () => {
    render(
        <App/>
    );

    userEvent.click(screen.getByTestId("BtnUp"));
    expect(screen.getByTestId("lblCurrentDay").textContent).toBe("2");
    userEvent.click(screen.getByTestId("BtnDown"));
    expect(screen.getByTestId("lblCurrentDay").textContent).toBe("1");

});


Next, we want to test that clicking BtnDown does nothing if the value is "1". So render App, and at the time of rendering, the value should be "1". If BtnDown is clicked, the value in lblCurrentDay should still be "1".

src/App/App.test.js
it("should react to up/down button", () => {
    render(
        <App/>
    );

    userEvent.click(screen.getByTestId("BtnUp"));
    expect(screen.getByTestId("lblCurrentDay").textContent).toBe("2");
    userEvent.click(screen.getByTestId("BtnDown"));
    expect(screen.getByTestId("lblCurrentDay").textContent).toBe("1");
});

it("should not react to down button if day is 1", () => {
    render(
        <App/>
    );

    userEvent.click(screen.getByTestId("BtnDown"));
    expect(screen.getByTestId("lblCurrentDay").textContent).toBe("1");
});


Naturally, what follows next is that we test to see if BtnUp does anything if the value is "12"! So render App, "click" BtnUp more than eleven times. The value should still be "12".

it("should not react to down button if day is 1", () => {
    render(
        <App/>
    );

    userEvent.click(screen.getByTestId("BtnDown"));
    expect(screen.getByTestId("lblCurrentDay").textContent).toBe("1");
});

it("should not react to up button if day is 12", () => {
    render(
        <App/>
    );

    userEvent.click(screen.getByTestId("BtnUp"));
    userEvent.click(screen.getByTestId("BtnUp"));
    userEvent.click(screen.getByTestId("BtnUp"));
    userEvent.click(screen.getByTestId("BtnUp"));
    userEvent.click(screen.getByTestId("BtnUp"));
    userEvent.click(screen.getByTestId("BtnUp"));
    userEvent.click(screen.getByTestId("BtnUp"));
    userEvent.click(screen.getByTestId("BtnUp"));
    userEvent.click(screen.getByTestId("BtnUp"));
    userEvent.click(screen.getByTestId("BtnUp"));
    userEvent.click(screen.getByTestId("BtnUp"));
    userEvent.click(screen.getByTestId("BtnUp"));
    userEvent.click(screen.getByTestId("BtnUp"));
    expect(screen.getByTestId("lblCurrentDay").textContent).toBe("12");
});


That was easy enough. Now we test the Row component. For this, we will not be testing button clicks, so there's no need to import the userEvent object.

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


Copy the content array over from Row.js, but remove the file properties. We just want the text. And start describing Row.

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

let content = [
    { text: '' },
    { text: 'a partridge in a pear tree!' },
    { text: 'two turtle doves, and' },
    { text: 'three french hens' },
    { text: 'four calling birds' },
    { text: 'five gold rings' },
    { text: 'six swans a-swimming'},
    { text: 'seven geese a-laying' },
    { text: 'eight maids a-milking' },
    { text: 'nine ladies dancing' },
    { text: 'ten lords a-leaping' },
    { text: 'eleven pipers piping' },
    { text: 'twelve drummers drumming' }
];


describe("Row", () => {

});


Next, we render Row. But remember that Row expects two attributes to be passed in - day and currentDay. Let's have day's value to be 2. currentDay can be any value because this test is for day and the value of currentDay doesn't matter.

src/components/Row/Row.test.js
describe("Row", () => {
    it("should display title according to day", () => {
        let day = 2;
        let currentDay = 1;

        render(
            <Row
                day={ day }
                currentDay={ currentDay }
            />
        );

    });
});


Here, we expect the text from the content array, pointed to by day, to be part of the DOM. Because that's what happens - the text is displayed according to the value of day.

src/components/Row/Row.test.js
describe("Row", () => {
    it("should display title according to day", () => {
        let day = 2;
        let currentDay = 1; // any value

        render(
            <Row
                day={ day }
                currentDay={ currentDay }
            />
        );

        expect(screen.queryByText(content[day].text)).toBeInTheDocument();
    });
});


The next test again tests the value of day. The number of SVGs displayed should equal the value of day. So we use the queryAllByTestId() method of the screen object, pass in "imgDay" as an argument, and test for the number of elements returned!

src/components/Row/Row.test.js
describe("Row", () => {
    it("should display title according to day", () => {
        let day = 2;
        let currentDay = 1; // any value

        render(
            <Row
                day={ day }
                currentDay={ currentDay }
            />
        );

        expect(screen.queryByText(content[day].text)).toBeInTheDocument();
    });

    it("should display number of images according to day", () => {
        let day = 2;
        let currentDay = 1; // any value

        render(
            <Row
                day={ day }
                currentDay={ currentDay }
            />
        );

        expect(screen.queryAllByTestId("imgDay")).toHaveLength(2);
    });

});


Don't forget to add the data-testid attribute!

src/components/Row/Row.js
<img data-testid="imgDay" key={ day+'img'+index } src={ content[day].file } width="100" height="100" />


Now the value of currentDay matters, because we're testing currentDay against day. Again, render Row with the same attributes.
src/components/Row/Row.test.js
describe("Row", () => {
    it("should display title according to day", () => {
        let day = 2;
        let currentDay = 1; // any value

        render(
            <Row
                day={ day }
                currentDay={ currentDay }
            />
        );

        expect(screen.queryByText(content[day].text)).toBeInTheDocument();
    });

    it("should display number of images according to day", () => {
        let day = 2;
        let currentDay = 1; // any value

        render(
            <Row
                day={ day }
                currentDay={ currentDay }
            />
        );

        expect(screen.queryAllByTestId("imgDay")).toHaveLength(2);
    });

    it("should be hidden if current day is lower than day", () => {
        let day = 2;
        let currentDay = 1;

        const { container } = render(
            <Row
                day={ day }
                currentDay={ currentDay }
            />
        );
    });

});


If currentDay is less than day, the class of Row should have Hidden.

src/components/Row/Row.test.js
it("should be hidden if current day is lower than day", () => {
    let day = 2;
    let currentDay = 1;

    const { container } = render(
        <Row
            day={ day }
            currentDay={ currentDay }
        />
    );

    expect(container.firstChild.classList.contains("Hidden")).toBe(true);
});



And finally, the reverse of that last test.

src/components/Row/Row.test.js

it("should be hidden if current day is lower than day", () => {
    let day = 2;
    let currentDay = 1;

    const { container } = render(
        <Row
            day={ day }
            currentDay={ currentDay }
        />
    );

    expect(container.firstChild.classList.contains("Hidden")).toBe(true);
});

it("should not be hidden if current day is not lower than day", () => {
    let day = 2;
    let currentDay = 3;

    const { container } = render(
        <Row
            day={ day }
            currentDay={ currentDay }
        />
    );

    expect(container.firstChild.classList.contains("Hidden")).toBe(false);
});


So when you run the test suite, you should see this.


That's it for testing. These are really simple tests, but then again this is a really simple app. There's always more you can do, but I think this range of tests is enough for now.

True love is so Row-mantic,
T___T

No comments:

Post a Comment