Testing

WebWriter can serve as a test runner. Packages may define and exports tests as package members. Each test module is loaded in an empty editor frame and results are reported in WebWriter’s UI. Optionally, tests can be rerun each time a source file changes (hot reloading).

In this setup, we will be using the browser version of Mocha, a popular test runner for JS. For details about Mocha, refer to the documentation.

Install dependencies

For the following steps to work, we need to add a few development dependencies.

Run in your project directory

npm install --save-dev mocha @types/mocha chai @webwriter/build

Create and export test module

As a first step, we need to create a test module file. This can go anywhere in your source. Typical choices are to either create a separate test directory, or to co-locate your tests with your source code.

For WebWriter to pick up your tests, you need to add them to your exports in your package.json. If you want your tests to be compiled (recommended), you need to point WebWriter to both the source file and the output path (same as with widgets).

Export test in package.json

{
    "exports": {
        "./tests/functionality.*": {
            "source": "./tests/functionality.test.ts",
            "default": "./dist/tests/functionality.*"
        }
        // ...
    }
    // ...
}

You can add as many test modules as you want - each will run in a separate frame.

Add Mocha boilerplate

In our test file, we need some setup code for Mocha to function.

./tests/functionality.test.ts: Boilerplate

import "mocha/mocha.js";
import {getMochaConfig} from "@webwriter/build/test"

mocha.setup(getMochaConfig())

// TESTS HERE

mocha.run()

Add tests

Now, we define our tests. For this, we make use of the assertion library Chai, which helps with writing concise tests.

For our first test, we simply want to check whether the widget is defined after it is added to the page. If there is an error in the constructor/connectedCallback or the element is not properly registered for example, this test will fail.

./tests/functionality.test.ts: Add tests

import "mocha/mocha.js";
import {getMochaConfig} from "@webwriter/build/test"
import { assert } from "chai" // Import Chai for assertions
import "../src/widgets/my-widget" // IMPORTANT: Import widget so custom element is registered!

mocha.setup(getMochaConfig())

describe("<my-widget>", function () {

  before(function () { // Fixture: Set up page for test
    document.body.insertAdjacentHTML("beforeend",
      `<my-widget></my-widget>`
    )
  }) 

  describe("initialize", function () { // add test suite
    it("is defined", async function () { // add test (should be async so it is run after widget has initialized itself)
      const el = document.querySelector("my-widget:defined")
      assert.isNotNull(el)
    })
  })
});

mocha.run()

Tests are treated as normal modules by WebWriter’s build tool, so you can write them in TypeScript and import code as usual. You can add as many suites/tests as you want in a single file.

Run tests

Finally, we can use WebWriter’s UI to run the test. Once you added your local package to WebWriter, switch to testing mode (enable ‘source commands’ in WebWriter’s options, then toggle with the ‘test’ button at the top right).

Next, you can run your tests either by clicking a button or have them run automatically each time a source file changes (use this with the @webwriter/build dev command for hot reloading). Test/fail results should be reported in the UI, and details can be checked in the console.

Other test runners

While Mocha is recommended and supported with a premade config, any test runners can be connected through a simple interface.

Essentially, WebWriter needs to receive events about the running of tests to display the report in the UI. For that, it listens for a custom test-update event on the test frame’s window. To support a test runner, we can simply fire that event at the appropriate point in the test running lifecycle.

beforeAll

This event indicates that the test runner is set up and tests are about to run (should fire once).

window.dispatchEvent(new CustomEvent("test-update", {detail: {
    type: "beforeAll"
}}))

beforeOne

This event indicates that a test in the test module is about to run (can fire multiple times).

window.dispatchEvent(new CustomEvent("test-update", {detail: {
    type: "beforeAll",
    id: "abc123", // unique ID for the test about to run
    path: ["<my-widget>", "API", "foo()"]  // path of labels for the test about to run (shown as a tree in the UI)
}}))

afterOne

This event indicates that a test in the test module has run (can fire multiple times).

window.dispatchEvent(new CustomEvent("test-update", {detail: {
    type: "beforeAll",
    id: "abc123", // unique ID for the test
    path: ["<my-widget>", "API", "foo()"],  // path of labels for the test (shown as a tree in the UI)
    passed: true, // whether the test passed or failed

    duration: 23, // OPTIONAL: Time the test took to complete
    sync: true, // OPTIONAL: Whether the test is synchronous or asynchronous code
    timedOut: false // OPTIONAL: Whether the test failed by timeout
}}))

afterAll

This event indicates that all tests in the test module have run (should fire once).

window.dispatchEvent(new CustomEvent("test-update", {detail: {
    type: "afterAll"
}}))