batr

Bundle and test CommonJS and ESM in NodeJS and UMD in the browser with AvaJS and Playwright. And repeat with Travis-CI.

Usage no npm install needed!

<script type="module">
  import batr from 'https://cdn.skypack.dev/batr';
</script>

README

batr

Bundle And Test ... and Repeat

batr-logo

Bundle and test CommonJS and ESM in NodeJS and UMD in the browser with Rollup, AvaJS and Playwright. And repeat with GitHub Actions workflow.

I'm using AvaJS since I want a simple enough test framework and don't want to be too smart about assertions. The needs are not that big. For UI tests it's good to be a little repetitive. If you want to test a sequence of interactions A, B, C and D, then test them all synchronously in one go. You'll get to test the transition between the interactions and that the result of interaction A, doesn't screw up interaction B and so on.

Example setup

For an actual working example, check out batr-example on how to use batr. It's an example library with minimal of functions and user-interface to show-case how to set up batr. The examples here are lifted from that library.

Libraries used:

Integrations

Get started

Add batr devDependency

All the dependencies in one. Security updates and version bumps done mostly at the start of every month, so less GitHub dependabot noise.

  "devDependencies": {
    "batr": "^1.0.5"
  }

The underlying libraries are used (required and imported) as normal.

Define main, module and browser

  • main - CJS - CommonJS
  • module - ESM - ES Modules
  • browser - UMD - Universal Module Definitions
  "main": "./dist/batr-example.cjs.js",
  "module": "./dist/batr-example.esm.mjs",
  "browser": "./dist/batr-example.umd.js",

Makes pointers to which files are used for what. Used i.e. when bundling correct distribution files with Rollup and to use the correct file when doing const moduleName = require('moduleName') or import moduleName from "moduleName".

Tests

Build/bundle and tests from package.json

  "scripts": {
    "build": "rollup --config",
    "test": "standard './*.js' && npm run build  && npx ava ./test/test.cjs.js && npx ava ./test/test.esm.mjs && npx ava ./test/ui-test.js"
  }

Rollup config for bundling CJS, ESM and UMD

import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import json from '@rollup/plugin-json'
import pkg from './package.json'

export default [
  // browser-friendly UMD build
  // CommonJS (for Node) and ES module (for bundlers) build.
  // (We could have three entries in the configuration array
  // instead of two, but it's quicker to generate multiple
  // builds from a single configuration where possible, using
  // an array for the `output` option, where we can specify
  // `file` and `format` for each target)
  {
    input: './src/index.js',
    output: [
      { name: 'math', file: pkg.browser, format: 'umd', exports: 'named' },
      { file: pkg.main, format: 'cjs' },
      { file: pkg.module, format: 'es' }
    ],
    plugins: [
      resolve(), // so Rollup can find `ms`
      commonjs(), // so Rollup can convert `ms` to an ES module
      json() // for Rollup to be able to read content from package.json
    ]
  }
]

Actual test scripts

Main - ./dist/batr-example.cjs.js
const test = require('ava')
const { add, subtract, multiply, divide } = require('../dist/batr-example.cjs.js')

test('addition a + b', (t) => {
  const expected = 31
  const addition = add(7, 24)
  t.deepEqual(addition, expected)
})

test('subtraction a - b', (t) => {
  const expected = -17
  const subtraction = subtract(7, 24)
  t.deepEqual(subtraction, expected)
})

test('multiplication a * b', (t) => {
  const expected = 168
  const multiplication = multiply(7, 24)
  t.deepEqual(multiplication, expected)
})

test('division a * b', (t) => {
  const expected = 0.2916666666666667
  const division = divide(7, 24)
  t.deepEqual(division, expected)
})
Module - ./dist/batr-example.esm.mjs

Same tests as for Main, just using import instead of require.

import test from 'ava'
import { add, subtract, multiply, divide } from '../dist/batr-example.esm.mjs'

// Tests are identical to Main/CJS tests
})
Browser - ./dist/ui-test.js

Similar tests, but done through recorded user interactions in a browser. You recorded with playwright codegen. Create your prototype and do something like this:

npx playwright codegen -o javascript index.html

Playwright has good documentation on how to record user interactions and generating test-code for different programming languages. I'm guessing it's good practice to swap some of the HTML references with a little more solid CSS selectors so that the tests won't fail becuase of small HTML changes.

To see more of what's going on you can set healess: false and slow it down with sloMo: 500, but it will fail if you try it on i.e. a server, since there it's running headless.

Also, you can test with different browsers or more than one browser, and emulate devices like an Iphone.

const { chromium } = require('playwright')
const test = require('ava')
const browserPromise = chromium.launch({
  headless: true
  // slowMo: 500
})

const path = require('path')
async function pageMacro (t, callback) {
  const browser = await browserPromise
  const page = await browser.newPage()
  await page.setViewportSize({ width: 640, height: 480 })
  try {
    await callback(t, page)
  } finally {
    await page.close()
  }
}

test('Add numbers 4 and 7, subtract 7 from 4, multiply 4 and finally divide 4 by 7', pageMacro, async (t, page) => {
  // t.plan(4)
  const filePath = await path.resolve('./demo/index.html')
  const url = 'file://' + filePath

  // Go to ./index.html
  await page.goto(url)

  // Click first number input field and delete
  await page.click('#firstNumber')
  await page.keyboard.press('Backspace')

  // Type number
  await page.keyboard.type('4')

  // Press Tab twice to get to next number
  await page.keyboard.press('Tab')
  await page.keyboard.press('Tab')

  // Fill #secondNumber
  await page.keyboard.type('7')

  // Press Tab with modifiers
  await page.press('#secondNumber', 'Shift+Tab')

  // screenshot, 1st task
  await page.screenshot({ path: './screenshots/screenshot-01.png' })

  // Test that 4 + 7 gives 11
  t.deepEqual(await page.textContent('#result span'), '11')

  // Select subtract
  await page.selectOption('select[name="calculation"]', 'subtract')

  // screenshot, 2nd task
  await page.screenshot({ path: './screenshots/screenshot-02.png' })

  // Test that 4 - 7 gives -3
  t.deepEqual(await page.textContent('#result span'), '-3')

  // Select multiply
  await page.selectOption('select[name="calculation"]', 'multiply')

  // screenshot, 3rd task
  await page.screenshot({ path: './screenshots/screenshot-03.png' })

  // Test that 3 * 11 gives 28
  t.deepEqual(await page.textContent('#result span'), '28')

  // Select divide
  await page.selectOption('select[name="calculation"]', 'divide')

  // screenshot, 4th task
  await page.screenshot({ path: './screenshots/screenshot-04.png' })

  // Test that 4 / 7 gives 0.5714285714285714
  t.deepEqual(await page.textContent('#result span'), '0.5714285714285714')
})

Continuous integration with GitHub Actions workflow

ubuntu-latest is easy going, but you can test OSX and Windows too. Check GitHubs runs-on documentiation. .github/workflows/tests.yml:

name: tests
on:
  - push
  - pull_request
jobs:
  run-tests:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [12.x, 14.x, 16.x]
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm install
      - run: sudo apt-get install xvfb
      - run: xvfb-run --auto-servernum npm test

Background and goal

  • Use less time on updating the same bundle and test framework code in different libraries.
  • Quicker bundling and test setup when creating new libraries.
  • As few dependencies as possible, or a good balance between dependencies and function, to not have minor updates all the time.
  • New NPM release every month, meaning less noise from Dependabot. Batr + dependencies will only be devDependencies, and security issues will not be a big problem.

Easy setup of

  • Ava tests in Node.js
  • Possibly duplicat Ava tests in browser
  • User-like interaction tests in browser, supported by Ava
  • Bundling & buildin g for the browser, CommonJS and ESM