/headless-concurrent-mocha

Using chrome headless and mocha to run react tests, concurrently

Primary LanguageTypeScriptMIT LicenseMIT

Headless chrome + mocha + react + typescript

An example project setup for running mocha tests in chrome headless.

You can checkout and run this project with:

npm install
npm test

Goal

This is a proof of concept for:

  • running mocha tests
  • with webpack, react, typescript
  • in chrome headless (without karma or jsdom)
  • concurrently

I have a few projects that are using Mocha, React, Typescript, and Webpack. I started this project to see how difficult it would be to run those mocha tests concurrently (each file in a separate process) in chrome headless. Moreoever, I want access to a real browser DOM in my tests and want to avoid Karma. In short, I want to have a test like this:

describe('A basic test', function () {
  let element: HTMLDivElement;

  before('setup', function () {
    element = document.createElement('div');
    document.body.appendChild(element);
  });

  after('teardown', function () {
    ReactDOM.unmountComponentAtNode(element);
    document.body.removeChild(element);
  });

  it('renders', function () {
    ReactDOM.render(<Greeting name="world" />, element);
    const intro = document.querySelector('.Greeting');
    expect(intro).to.exist;
    expect(intro!.textContent).to.contain('Hello, world');
  });
});

Motivation

At the time of writing (dec 2017):

I found Karma to be confusing to set-up and difficult to debug / maintain, and wasn't ready to migrate my existing projects to Jest. I suspected manually setting up chrome headless w/ pupeteer would be a small amount of work for existing mocha and webpack projects and wanted to validate that hypothesis.

Code overview

  • Compile each test file into an independent test output file
  • Launch chrome headless and one or more tabs
  • Inject mocha and a test file into each tab
  • Use a custom mocha-reporter in each tab to collect test results
  • Pass completed test results from each file to reporter
  • Print out a final report on number of passes and failures

A few noteworthy points about this setup.

  1. Test files are compiled from multiple test input files into multiple test output files, rather than a single test bundle. Webpack's entry configuration can run asynchronously, allowing us to find all test files dynamically:
  // webpack.config.ts

  entry: async (): Promise<any> => {
    const { stdout } = await execa('find', ['src', '-type', 'f', '-name', '*.spec.*']);
    const testfiles = stdout.split('\n');

    return testfiles.reduce((entryMap: EntryMap, filename: string) => {
      const source = path.resolve(__dirname, '..', filename);

      entryMap[filename] = source;
      return entryMap;
    }, {});
  },
  1. Test file's and dependencies are loaded into tabs on demand
  // runner.ts

  const page = await this.browser.newPage();
  // ...
  await page.addScriptTag({
    path: path.resolve(__dirname, '../node_modules/mocha/mocha.js')
  })

  await page.evaluate(createMochaReporter);

  for (const bundle of test.bundles) {
    await page.addScriptTag({
      path: bundle,
    })
  }

  await page.evaluate(runMocha);

This could be extended to load test from memory (instead of output files), or to use different test frameworks via configuration.

  1. Mocha expects to run all tests to completion and report as it goes. Because we want to run each test file in isolation, in parallel, and not output test results from different suites at the same time, we use two custom reporters:
  • mocha-reporter.js is injected into the browser, and collects, rather than prints out, test results as they run
  • local-reporter.ts prints out the collected results, styled similar to mocha's spec reporter.
  • runner.ts coordinates passing the collected results from mocha-reporter.js to local-reporter.ts. mocha-reporter.js stores test results window.__TEST_RESULT__ so they can be accessed when complete:
  // runner.ts

  await page.waitForFunction(() => (window as any).__TEST_RESULT__);
  const resultHandle = await page.evaluateHandle(() => (window as any).__TEST_RESULT__);
  const testResults = await resultHandle.jsonValue() as MochaRunResult;
  // ...

  reportTests(test.entry, testResults);

Links