Skip to content

Testing Web React applications

Vladimir Turov edited this page Sep 21, 2020 · 2 revisions

In brief

In our web testing library hs-test-web there is a possibility to test frontend applications based on React library.

React tests

If you want to develop code problems that are meant to be tested via Hyperskill's web interface then you should treat every stage as a separate individual code problem.

As React code requires compilation before running in a web browser tests should be slightly adjusted and not use file://... syntax to open html files as with regular web projects. To compile and host React application the hs-test-web-server repository is used.

Setup React template

We'll start from the end of the regular JavaScript project setup.

Your setup so far should look like this:

Here's package.json content used for React projects:

{
  "devDependencies": {
    "jest": "^24.9.0",
    "@types/jest": "^24.9.0",
    "puppeteer": "^2.0.0",
    "hs-test-web": "https://github.com/hyperskill/hs-test-web/archive/v1.tar.gz",
    "hs-test-web-server": "https://github.com/hyperskill/hs-test-web-server/archive/v1.tar.gz",
    "react": "^16.13.1",
    "react-dom": "^16.13.1"
  },
  "scripts": {
    "start": "node node_modules/hs-test-web-server/start.js"
  }
}

Note: currently, here's a list of all the dependencies besides jest and puppeteer that is installed for testing React applications on Hyperskill. You shouldn't use other dependencies if you are writing tests to be graded in web interface. Contact Hyperksill so we can add other useful dependencies.

You should add an initial template for the user, so they can start developing the application right away. The initial template should be written in React and can be run without any compilation errors. A React's official initial template will work just fine.

Now your setup may look like follows:

If you see the addition (excluded) near your files, you should right-click on the file and select Course Creator -> Include into Task

Note: On Hyperskill's web interface, only a single file can be shown, so you should mark files that users shouldn't change as hidden. Normally, in a regular project, only tests are hidden but keeping in mind that users should solve this problem via web interface you should leave only one file as visible. So, tests and all files except one JSX file should be hidden. This implies that all the hidden files should already contain proper code. You can hide files right-clicking and selecting Course Creator -> Hide from Learner

Also, as you might know, React uses a single html template and fills it with all the necessary data during compilation. You should also create such file. It should be located in the public folder in the root of the Web Storm project, near the package.json file. Name this file index.html

All fill it with the following content (don't change it if you are developing code problems based on React that is meant to be tested via Hyperskill's web interface):

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>React</title>
</head>
<body>
<div id="root"></div>
<script src="main.js"></script>
</body>
</html>

Example

Testing React applications is not that different compared to testing regular web projects. All the differences are listed below:

  1. Instead of path to the html file (await page.goto("file://" + path);) use host and port (await page.goto('http://localhost:3010');)
  2. Import hs-test-web-react (const react = require("hs-test-web-server");)
  3. Instead of let result = await stageTest(); use the following (you can use different port):
let result = await react.startServerAndTest(
    'localhost', 3010, path.resolve(__dirname, '..'), stageTest
);

And other than that, no differences in testing. Below are real tests for the first stage of Minesweeper React project.

const puppeteer = require('puppeteer');
const path = require('path');

const hs = require('hs-test-web');
const react = require("hs-test-web-server");

const sleep = (ms) => new Promise(res => setTimeout(res, ms));

async function stageTest() {
    const browser = await puppeteer.launch({
        headless: false,
        defaultViewport: null,
        args:['--start-maximized', '--disable-infobar'],
        ignoreDefaultArgs: ['--enable-automation'],
    });

    const page = await browser.newPage();
    await page.goto('http://localhost:3010');

    page.on('console', msg => console.log(msg.text()));

    let result = await hs.testPage(page,
        () => {
            if (document.getElementById('root').textContent === "Minesweeper is loading...") {
                return hs.correct();
            } else {
                return hs.wrong("There should be a text 'Minesweeper is loading...' ");
            }
        },
        () => {
            let result = hs.wrong("The font should be changed.");

            Array.from(document.getElementsByTagName("*")).forEach( element => {
                if (element.tagName !== "HTML" && element.innerText === 'Minesweeper is loading...') {
                    if (window.getComputedStyle(element).fontFamily !== "-apple-system, BlinkMacSystemFont, " +
                        "\"Segoe UI\", Roboto, Oxygen, Ubuntu, Cantarell, \"Fira Sans\", \"Droid Sans\", " +
                        "\"Helvetica Neue\", sans-serif") {
                        
                        result = hs.correct()
                    }
                }
            });

            return result;
        },
        () => {
            let imgs = document.getElementsByTagName('img');

            if (imgs.length !== 1) {
                return hs.wrong("Only one picture should be on the page")
            }

            let canvas = document.createElement('canvas');
            canvas.width = imgs[0].width;
            canvas.height = imgs[0].height;
            canvas.getContext('2d').drawImage(imgs[0], 0,0,imgs[0].width, imgs[0].height)
            let pixelColor = canvas.getContext('2d').getImageData(imgs[0].width/2,imgs[0].height/2, 1, 1).data
            let logoPixelColor = [97,218,251];
            if (logoPixelColor[0] === pixelColor[0] &&
                logoPixelColor[1] === pixelColor[1] &&
                logoPixelColor[2] === pixelColor[2]) {
                return hs.wrong("There shouldn't be the React logo on the page")
            }

            return hs.correct();
        },
        () => {
            let result = hs.wrong();

            Array.from(document.getElementsByTagName("*")).forEach( element => {
                if ( element.children.length === 2 && element.innerText === "Minesweeper is loading...") {
                    let style = window.getComputedStyle(element);
                    if (style.display === "flex" &&
                        style.flexDirection === "column" &&
                        style.alignItems === "center" &&
                        style.justifyContent === "center") {
                        result = hs.correct();
                    }
                }
            });

            return result;
        }
    );

    await browser.close();
    return result;


}

jest.setTimeout(30000);
test("Test stage", async () => {
    let result = await react.startServerAndTest(
        'localhost', 3010, path.resolve(__dirname, '..'), stageTest
    );

    if (result['type'] === 'wrong') {
        fail(result['message']);
    }
});