Skip to content
/ batr Public

Bundle and test CommonJS and ESM in NodeJS and UMD in the browser with AvaJS and Playwright. And repeat with i.e GitHub CI.

License

Notifications You must be signed in to change notification settings

eklem/batr

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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.

A little blogpost on why I thought Batr was a good idea: Test setup for JavaScript/web development with less stress and pain— My solution so far: Batr.

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.

NPM version NPM downloads Build Status MIT License

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