Testing
As of 0.4.0, Hex Engine includes the Test-It test runner. You can use this for unit tests, visual regression testing, debugging, and more.
To add tests to your codebase, create files whose filenames end with .test.ts
, .test.tsx
, .test.js
, or .test.jsx
. Those files will be run by Test-It when you run npm test
.
Note: If your codebase was created with a version of Hex Engine prior to 0.4.0, then there won't be a
test
script in yourpackage.json
, sonpm test
won't work. To fix it, create a new project by following the instructions in First Time Setup, then copy your game files from your existing codebase into the new project.
You can run your tests in watch mode by passing the --watch
flag, ie npm test -- --watch
.
You can update any snapshots created by expect(value).toMatchSnapshot()
or expect(buffer).toMatchImageSnapshot()
by passing the -u
flag, ie npm test -- -u
.
Example Tests
Below are some examples of tests you could add to your codebase. Each is explained, and has a comment on the first line indicating an appropriate filename for the test file where the code should appear.
Whole App VRT
This example test starts the game, advances it 100 frames, and then takes a screenshot of the page. The first time the test is run, the screenshot will be taken and saved to disk. On subsequent runs, the screenshot will be taken and compared to the screenshot on disk, and the test will fail if the screenshot differs.
This test assumes your root component is accessible via ./Root
.
// frame100.test.tsimport { createRoot, RunLoop } from "@hex-engine/2d";import Root from "./Root";test("frame 100 renders as expected", async () => {const rootEnt = createRoot(Root);const runLoop = rootEnt.getComponent(RunLoop)!;runLoop.pause(); // This is the same as hitting the pause button in the Hex Engine inspector.for (let i = 0; i < 100; i++) {runLoop.step(); // This advances to the next frame; it's the same as hitting the step button in the Hex Engine inspector.}expect(await TestIt.captureScreenshot()).toMatchImageSnapshot();});
If your tests will be run across different operating systems, then the font for the inspector may render differently. As such, you may wish to hide the inspector UI prior to taking your screenshot. You can do that as follows:
// frame100.test.tsimport { createRoot, RunLoop } from "@hex-engine/2d";import Inspector from "@hex-engine/inspector"; // Add inspector importimport Root from "./Root";test("frame 100 renders as expected", async () => {const rootEnt = createRoot(Root);const runLoop = rootEnt.getComponent(RunLoop)!;runLoop.pause();for (let i = 0; i < 100; i++) {runLoop.step();}const inspector = rootEnt.getComponent(Inspector)!;inspector.hide(); // Hide the inspector UIexpect(await TestIt.captureScreenshot()).toMatchImageSnapshot();});
Unit Test
This example test creates an empty canvas and tests a single component in isolation. It sets up a root entity with a canvas and the component under test, and then takes a screenshot of the page. The first time the test is run, the screenshot will be taken and saved to disk. On subsequent runs, the screenshot will be taken and compared to the screenshot on disk, and the test will fail if the screenshot differs.
This test assumes that the component you want to test is accessible via ./MyComponent
.
// MyComponent.test.tsimport { createRoot, useNewComponent, Canvas, RunLoop } from "@hex-engine/2d";import Inspector from "@hex-engine/inspector";import MyComponent from "./MyComponent";test("MyComponent renders as expected", async () => {const rootEnt = createRoot(() => {useNewComponent(() => Canvas({ backgroundColor: "white" }));useNewComponent(() => MyComponent());});const runLoop = rootEnt.getComponent(RunLoop)!;runLoop.pause();const inspector = rootEnt.getComponent(Inspector)!;inspector.hide();expect(await TestIt.captureScreenshot()).toMatchImageSnapshot();});
Debugging Tests
To debug a test using Chrome DevTools, instead of using test
or it
when declaring your test, use debug
. For example, to debug the "Whole App VRT" example test, you would modify line 5 as highlighted here:
// frame100.test.tsimport { createRoot, RunLoop } from "@hex-engine/2d";import Root from "./Root";debug("frame 100 renders as expected", async () => { // test changed to debugconst rootEnt = createRoot(Root);const runLoop = rootEnt.getComponent(RunLoop)!;runLoop.pause();for (let i = 0; i < 100; i++) {runLoop.step();}expect(await TestIt.captureScreenshot()).toMatchImageSnapshot();});
With this change in place, when you run your tests, two windows will open up: A browser window showing the contents of the page, and a Chrome DevTools window. You can use the Chrome DevTools to debug the test.
Running Tests on a Continuous Integration (CI) Server/Platform
Test-It, the test runner that Hex Engine uses, cannot run tests in headless mode. Instead, it runs them using a hidden Chromium window. Due to this, your tests may not work out-of-the-box on a CI server/platform such as Jenkins, Travis, or GitHub Actions. One way to make them work is to run them in a docker container that has all the necessary dependencies set up for running browser tests with Chrome. Here is an example Dockerfile that you can use to build an image for such a container.
FROM node:currentENV DEBIAN_FRONTEND=noninteractiveRUN apt-get updateRUN apt-get -y install localesRUN locale-gen en_US.UTF-8RUN update-locale LANG=en_US.UTF-8ENV LANG en_US.UTF-8RUN apt-get install -y libglib2.0RUN apt-get install -y libnss3RUN apt-get install -y xvfbRUN apt-get install -y libxcomposite1RUN apt-get install -y libxcursor1RUN apt-get install -y libxi6RUN apt-get install -y libxtst6RUN apt-get install -y libcups2RUN apt-get install -y libxss1RUN apt-get install -y libxrandr2RUN apt-get install -y libasound2RUN apt-get install -y libatk1.0RUN apt-get install -y libpangocairo-1.0RUN apt-get install -y libatspi2.0RUN apt-get install -y libgtk-3-0ENV DISPLAY :0CMD Xvfb -screen 0 1024x768x16 -ac & cd /app && npm test