diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 0f34c9e3..869277fa 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -43,17 +43,8 @@ jobs:
       - run: npm install
         env:
           PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
-      - run: npm test
+      - run: npm run test
       - run: npm run coverage
-  esm:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-node@v1
-        with:
-          node-version: 14
-      - run: npm install
-      - run: npm run test:esm
   deno:
     runs-on: ubuntu-latest
     steps:
diff --git a/deno.ts b/deno.ts
index a94e49f3..3fa550f0 100644
--- a/deno.ts
+++ b/deno.ts
@@ -1,13 +1,13 @@
 // Bootstrap cliui with CommonJS dependencies:
 import { cliui, UI } from './build/lib/index.js'
 import type { UIOptions } from './build/lib/index.d.ts'
-import { wrap, stripAnsi } from './build/lib/string-utils.js'
+import wrap from 'wrap-ansi'
+import stripAnsi from 'strip-ansi'
+import stringWidth from 'string-width'
 
 export default function ui (opts: UIOptions): UI {
   return cliui(opts, {
-    stringWidth: (str: string) => {
-      return [...str].length
-    },
+    stringWidth,
     stripAnsi,
     wrap
   })
diff --git a/index.mjs b/index.mjs
index bc7a022b..720cb523 100644
--- a/index.mjs
+++ b/index.mjs
@@ -1,12 +1,12 @@
 // Bootstrap cliui with CommonJS dependencies:
 import { cliui } from './build/lib/index.js'
-import { wrap, stripAnsi } from './build/lib/string-utils.js'
+import wrap from 'wrap-ansi'
+import stripAnsi from 'strip-ansi'
+import stringWidth from 'string-width'
 
 export default function ui (opts) {
   return cliui(opts, {
-    stringWidth: (str) => {
-      return [...str].length
-    },
+    stringWidth,
     stripAnsi,
     wrap
   })
diff --git a/lib/cjs.ts b/lib/cjs.ts
index bda4241a..7c68d6bf 100644
--- a/lib/cjs.ts
+++ b/lib/cjs.ts
@@ -1,8 +1,9 @@
 // Bootstrap cliui with CommonJS dependencies:
 import { cliui, UIOptions } from './index.js'
-const stringWidth = require('string-width')
-const stripAnsi = require('strip-ansi')
-const wrap = require('wrap-ansi')
+import stringWidth from 'string-width'
+import wrap from 'wrap-ansi'
+import stripAnsi from 'strip-ansi'
+
 export default function ui (opts: UIOptions) {
   return cliui(opts, {
     stringWidth,
diff --git a/lib/string-utils.ts b/lib/string-utils.ts
deleted file mode 100644
index 23d78fdb..00000000
--- a/lib/string-utils.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-// Minimal replacement for ansi string helpers "wrap-ansi" and "strip-ansi".
-// to facilitate ESM and Deno modules.
-// TODO: look at porting https://www.npmjs.com/package/wrap-ansi to ESM.
-
-// The npm application
-// Copyright (c) npm, Inc. and Contributors
-// Licensed on the terms of The Artistic License 2.0
-// See: https://github.com/npm/cli/blob/4c65cd952bc8627811735bea76b9b110cc4fc80e/lib/utils/ansi-trim.js
-const ansi = new RegExp('\x1b(?:\\[(?:\\d+[ABCDEFGJKSTm]|\\d+;\\d+[Hfm]|' +
-'\\d+;\\d+;\\d+m|6n|s|u|\\?25[lh])|\\w)', 'g')
-
-export function stripAnsi (str: string) {
-  return str.replace(ansi, '')
-}
-
-export function wrap (str: string, width: number) {
-  const [start, end] = str.match(ansi) || ['', '']
-  str = stripAnsi(str)
-  let wrapped = ''
-  for (let i = 0; i < str.length; i++) {
-    if (i !== 0 && (i % width) === 0) {
-      wrapped += '\n'
-    }
-    wrapped += str.charAt(i)
-  }
-  if (start && end) {
-    wrapped = `${start}${wrapped}${end}`
-  }
-  return wrapped
-}
diff --git a/package.json b/package.json
index eab6bf47..6dcd1aa6 100644
--- a/package.json
+++ b/package.json
@@ -18,10 +18,9 @@
     "check": "standardx '**/*.ts' && standardx '**/*.js' && standardx '**/*.cjs'",
     "fix": "standardx --fix '**/*.ts' && standardx --fix '**/*.js' && standardx --fix '**/*.cjs'",
     "pretest": "rimraf build && tsc -p tsconfig.test.json && cross-env NODE_ENV=test npm run build:cjs",
-    "test": "c8 mocha ./test/*.cjs",
-    "test:esm": "c8 mocha ./test/esm/cliui-test.mjs",
+    "test": "c8 --exclude-after-remap mocha ./test/cjs/*.cjs ./test/esm/*.mjs",
     "postest": "check",
-    "coverage": "c8 report --check-coverage",
+    "coverage": "c8 --exclude-after-remap report --check-coverage",
     "precompile": "rimraf build",
     "compile": "tsc",
     "postcompile": "npm run build:cjs",
@@ -49,11 +48,13 @@
   "author": "Ben Coe <ben@npmjs.com>",
   "license": "ISC",
   "dependencies": {
-    "string-width": "^4.2.0",
-    "strip-ansi": "^6.0.1",
-    "wrap-ansi": "^7.0.0"
+    "string-width": "^5.1.2",
+    "strip-ansi": "^7.0.1",
+    "wrap-ansi": "^8.1.0"
   },
   "devDependencies": {
+    "@rollup/plugin-commonjs": "^22.0.2",
+    "@rollup/plugin-node-resolve": "^14.1.0",
     "@types/node": "^14.0.27",
     "@typescript-eslint/eslint-plugin": "^4.0.0",
     "@typescript-eslint/parser": "^4.0.0",
@@ -65,12 +66,12 @@
     "eslint-plugin-import": "^2.22.0",
     "eslint-plugin-node": "^11.1.0",
     "gts": "^3.0.0",
-    "mocha": "^10.0.0",
+    "mocha": "^9.2.2",
     "rimraf": "^3.0.2",
-    "rollup": "^2.23.1",
-    "rollup-plugin-ts": "^3.0.2",
+    "rollup": "^2.79.1",
+    "rollup-plugin-ts": "^2.0.7",
     "standardx": "^7.0.0",
-    "typescript": "^4.0.0"
+    "typescript": "4.6.4"
   },
   "files": [
     "build",
diff --git a/rollup.config.js b/rollup.config.js
index fed35314..f6737cd1 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -1,4 +1,10 @@
 import ts from 'rollup-plugin-ts'
+import nodeResolve from '@rollup/plugin-node-resolve'
+import commonjs from '@rollup/plugin-commonjs'
+
+// These two transient dependencies are still CommonJS:
+// 'node_modules/emoji-regex/index.js',
+// 'node_modules/eastasianwidth/eastasianwidth.js'
 
 const output = {
   format: 'cjs',
@@ -12,6 +18,8 @@ export default {
   input: './lib/cjs.ts',
   output,
   plugins: [
-    ts({ /* options */ })
+    ts({ /* options */ }),
+    nodeResolve(),
+    commonjs()
   ]
 }
diff --git a/test/cjs/cliui-test.cjs b/test/cjs/cliui-test.cjs
new file mode 100644
index 00000000..8f73d38d
--- /dev/null
+++ b/test/cjs/cliui-test.cjs
@@ -0,0 +1,4 @@
+const { runTests } = require('../shared-tests.cjs')
+const cliui = require('../../build/index.cjs')
+
+runTests(cliui, 'CJS')
diff --git a/test/cliui.cjs b/test/cliui.cjs
deleted file mode 100644
index 6dd86e7e..00000000
--- a/test/cliui.cjs
+++ /dev/null
@@ -1,492 +0,0 @@
-'use strict'
-
-/* global describe, it */
-
-require('chai').should()
-
-// Force chalk to enable color, if it's disabled the test fails.
-process.env.FORCE_COLOR = 1
-
-const chalk = require('chalk')
-const cliui = require('../build/index.cjs')
-const stripAnsi = require('strip-ansi')
-
-describe('cliui', () => {
-  describe('resetOutput', () => {
-    it('should set lines to empty', () => {
-      const ui = cliui()
-      ui.div('i am a value that would be in a line')
-      ui.resetOutput()
-      ui.toString().length.should.be.equal(0)
-    })
-  })
-
-  describe('div', () => {
-    it("wraps text at 'width' if a single column is given", () => {
-      const ui = cliui({
-        width: 10
-      })
-
-      ui.div('i am a string that should be wrapped')
-
-      ui.toString().split('\n').forEach(row => {
-        row.length.should.be.lte(10)
-      })
-    })
-
-    it('evenly divides text across columns if multiple columns are given', () => {
-      const ui = cliui({
-        width: 40
-      })
-
-      ui.div(
-        { text: 'i am a string that should be wrapped', width: 15 },
-        'i am a second string that should be wrapped',
-        'i am a third string that should be wrapped'
-      )
-
-      // total width of all columns is <=
-      // the width cliui is initialized with.
-      ui.toString().split('\n').forEach(row => {
-        row.length.should.be.lte(40)
-      })
-
-      // it should wrap each column appropriately.
-      const expected = [
-        'i am a string  i am a      i am a third',
-        'that should be second      string that',
-        'wrapped        string that should be',
-        '               should be   wrapped',
-        '               wrapped'
-      ]
-
-      ui.toString().split('\n').should.eql(expected)
-    })
-
-    it('allows for a blank row to be appended', () => {
-      const ui = cliui({
-        width: 40
-      })
-
-      ui.div()
-
-      // it should wrap each column appropriately.
-      const expected = ['']
-
-      ui.toString().split('\n').should.eql(expected)
-    })
-  })
-
-  describe('_columnWidths', () => {
-    it('uses same width for each column by default', () => {
-      const ui = cliui({
-        width: 40
-      })
-      const widths = ui.columnWidths([{}, {}, {}])
-
-      widths[0].should.equal(13)
-      widths[1].should.equal(13)
-      widths[2].should.equal(13)
-    })
-
-    it('divides width over remaining columns if first column has width specified', () => {
-      const ui = cliui({
-        width: 40
-      })
-      const widths = ui.columnWidths([{ width: 20 }, {}, {}])
-
-      widths[0].should.equal(20)
-      widths[1].should.equal(10)
-      widths[2].should.equal(10)
-    })
-
-    it('divides width over remaining columns if middle column has width specified', () => {
-      const ui = cliui({
-        width: 40
-      })
-      const widths = ui.columnWidths([{}, { width: 10 }, {}])
-
-      widths[0].should.equal(15)
-      widths[1].should.equal(10)
-      widths[2].should.equal(15)
-    })
-
-    it('keeps track of remaining width if multiple columns have width specified', () => {
-      const ui = cliui({
-        width: 40
-      })
-      const widths = ui.columnWidths([{ width: 20 }, { width: 12 }, {}])
-
-      widths[0].should.equal(20)
-      widths[1].should.equal(12)
-      widths[2].should.equal(8)
-    })
-
-    it('uses a sane default if impossible widths are specified', () => {
-      const ui = cliui({
-        width: 40
-      })
-      const widths = ui.columnWidths([{ width: 30 }, { width: 30 }, { padding: [0, 2, 0, 1] }])
-
-      widths[0].should.equal(30)
-      widths[1].should.equal(30)
-      widths[2].should.equal(4)
-    })
-  })
-
-  describe('alignment', () => {
-    it('allows a column to be right aligned', () => {
-      const ui = cliui({
-        width: 40
-      })
-
-      ui.div(
-        'i am a string',
-        { text: 'i am a second string', align: 'right' },
-        'i am a third string that should be wrapped'
-      )
-
-      // it should right-align the second column.
-      const expected = [
-        'i am a stringi am a secondi am a third',
-        '                    stringstring that',
-        '                          should be',
-        '                          wrapped'
-      ]
-
-      ui.toString().split('\n').should.eql(expected)
-    })
-
-    it('allows a column to be center aligned', () => {
-      const ui = cliui({
-        width: 60
-      })
-
-      ui.div(
-        'i am a string',
-        { text: 'i am a second string', align: 'center', padding: [0, 2, 0, 2] },
-        'i am a third string that should be wrapped'
-      )
-
-      // it should right-align the second column.
-      const expected = [
-        'i am a string          i am a second       i am a third string',
-        '                           string          that should be',
-        '                                           wrapped'
-      ]
-
-      ui.toString().split('\n').should.eql(expected)
-    })
-  })
-
-  describe('padding', () => {
-    it('handles left/right padding', () => {
-      const ui = cliui({
-        width: 40
-      })
-
-      ui.div(
-        { text: 'i have padding on my left', padding: [0, 0, 0, 4] },
-        { text: 'i have padding on my right', padding: [0, 2, 0, 0], align: 'center' },
-        { text: 'i have no padding', padding: [0, 0, 0, 0] }
-      )
-
-      // it should add left/right padding to columns.
-      const expected = [
-        '    i have     i have      i have no',
-        '    padding  padding on    padding',
-        '    on my     my right',
-        '    left'
-      ]
-
-      ui.toString().split('\n').should.eql(expected)
-    })
-
-    it('handles top/bottom padding', () => {
-      const ui = cliui({
-        width: 40
-      })
-
-      ui.div(
-        'i am a string',
-        { text: 'i am a second string', padding: [2, 0, 0, 0] },
-        { text: 'i am a third string that should be wrapped', padding: [0, 0, 1, 0] }
-      )
-
-      // it should add top/bottom padding to second
-      // and third columns.
-      const expected = [
-        'i am a string             i am a third',
-        '                          string that',
-        '             i am a secondshould be',
-        '             string       wrapped',
-        ''
-      ]
-
-      ui.toString().split('\n').should.eql(expected)
-    })
-
-    it('preserves leading whitespace as padding', () => {
-      const ui = cliui()
-
-      ui.div('     LEADING WHITESPACE')
-      ui.div('\u001b[34m     with ansi\u001b[39m')
-
-      const expected = [
-        '     LEADING WHITESPACE',
-        '     with ansi'
-      ]
-
-      ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected)
-    })
-  })
-
-  describe('border', () => {
-    it('allows a border to be placed around a div', () => {
-      const ui = cliui({
-        width: 40
-      })
-
-      ui.div(
-        { text: 'i am a first string', padding: [0, 0, 0, 0], border: true },
-        { text: 'i am a second string', padding: [1, 0, 0, 0], border: true }
-      )
-
-      const expected = [
-        '.------------------.',
-        '| i am a first     |.------------------.',
-        '| string           || i am a second    |',
-        "'------------------'| string           |",
-        "                    '------------------'"
-      ]
-
-      ui.toString().split('\n').should.eql(expected)
-    })
-  })
-
-  describe('wrap', () => {
-    it('allows wordwrap to be disabled', () => {
-      const ui = cliui({
-        wrap: false
-      })
-
-      ui.div(
-        { text: 'i am a string', padding: [0, 1, 0, 0] },
-        { text: 'i am a second string', padding: [0, 2, 0, 0] },
-        { text: 'i am a third string that should not be wrapped', padding: [0, 0, 0, 2] }
-      )
-
-      ui.toString().should.equal('i am a string i am a second string    i am a third string that should not be wrapped')
-    })
-  })
-
-  describe('span', () => {
-    it('appends the next row to the end of the prior row if it fits', () => {
-      const ui = cliui({
-        width: 40
-      })
-
-      ui.span(
-        { text: 'i am a string that will be wrapped', width: 30 }
-      )
-
-      ui.div(
-        { text: ' [required] [default: 99]', align: 'right' }
-      )
-
-      const expected = [
-        'i am a string that will be',
-        'wrapped         [required] [default: 99]'
-      ]
-
-      ui.toString().split('\n').should.eql(expected)
-    })
-
-    it('does not append the string if it does not fit on the prior row', () => {
-      const ui = cliui({
-        width: 40
-      })
-
-      ui.span(
-        { text: 'i am a string that will be wrapped', width: 30 }
-      )
-
-      ui.div(
-        { text: 'i am a second row', align: 'left' }
-      )
-
-      const expected = [
-        'i am a string that will be',
-        'wrapped',
-        'i am a second row'
-      ]
-
-      ui.toString().split('\n').should.eql(expected)
-    })
-
-    it('always appends text to prior span if wrap is disabled', () => {
-      const ui = cliui({
-        wrap: false,
-        width: 40
-      })
-
-      ui.span(
-        { text: 'i am a string that will be wrapped', width: 30 }
-      )
-
-      ui.div(
-        { text: 'i am a second row', align: 'left', padding: [0, 0, 0, 3] }
-      )
-
-      ui.div('a third line')
-
-      const expected = [
-        'i am a string that will be wrapped   i am a second row',
-        'a third line'
-      ]
-
-      ui.toString().split('\n').should.eql(expected)
-    })
-
-    it('appends to prior line appropriately when strings contain ansi escape codes', () => {
-      const ui = cliui({
-        width: 40
-      })
-
-      ui.span(
-        { text: chalk.green('i am a string that will be wrapped'), width: 30 }
-      )
-
-      ui.div(
-        { text: chalk.blue(' [required] [default: 99]'), align: 'right' }
-      )
-
-      const expected = [
-        'i am a string that will be',
-        'wrapped         [required] [default: 99]'
-      ]
-
-      ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected)
-    })
-  })
-
-  describe('layoutDSL', () => {
-    it('turns tab into multiple columns', () => {
-      const ui = cliui({
-        width: 60
-      })
-
-      ui.div(
-        '  <regex>  \tmy awesome regex\n  <my second thing>  \tanother row\t  a third column'
-      )
-
-      const expected = [
-        '  <regex>            my awesome regex',
-        '  <my second thing>  another row          a third column'
-      ]
-
-      ui.toString().split('\n').should.eql(expected)
-    })
-
-    it('turns newline into multiple rows', () => {
-      const ui = cliui({
-        width: 40
-      })
-
-      ui.div(
-        'Usage: $0\n  <regex>\t  my awesome regex\n  <glob>\t  my awesome glob\t  [required]'
-      )
-      const expected = [
-        'Usage: $0',
-        '  <regex>  my awesome regex',
-        '  <glob>   my awesome     [required]',
-        '           glob'
-      ]
-
-      ui.toString().split('\n').should.eql(expected)
-    })
-
-    it('aligns rows appropriately when they contain ansi escape codes', () => {
-      const ui = cliui({
-        width: 40
-      })
-
-      ui.div(
-        '  <regex>\t  ' + chalk.red('my awesome regex') + '\t  [regex]\n  ' + chalk.blue('<glob>') + '\t  my awesome glob\t  [required]'
-      )
-
-      const expected = [
-        '  <regex>  my awesome     [regex]',
-        '           regex',
-        '  <glob>   my awesome     [required]',
-        '           glob'
-      ]
-
-      ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected)
-    })
-
-    it('ignores ansi escape codes when measuring padding', () => {
-      // Forcefully enable color-codes for this test
-      const { enabled, level } = chalk
-      chalk.enabled = true
-      chalk.level = 1
-
-      const ui = cliui({
-        width: 25
-      })
-
-      // using figlet font 'Shadow' rendering of text 'true' here
-      ui.div(
-        chalk.blue('  |                      \n  __|   __|  |   |   _ \\ \n  |    |     |   |   __/ \n \\__| _|    \\__,_| \\___| \n                         ')
-      )
-
-      // relevant part is first line - leading whitespace should be preserved as left padding
-      const expected = [
-        '  |',
-        '  __|   __|  |   |   _ \\',
-        '  |    |     |   |   __/',
-        ' \\__| _|    \\__,_| \\___|',
-        '                         '
-      ]
-
-      ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected)
-      chalk.enabled = enabled
-      chalk.level = level
-    })
-
-    it('correctly handles lack of ansi escape codes when measuring padding', () => {
-      const ui = cliui({
-        width: 25
-      })
-
-      // using figlet font 'Shadow' rendering of text 'true' here
-      ui.div(
-        '  |                      \n  __|   __|  |   |   _ \\ \n  |    |     |   |   __/ \n \\__| _|    \\__,_| \\___| \n                         '
-      )
-
-      // The difference
-      const expected = [
-        '  |',
-        '  __|   __|  |   |   _ \\',
-        '  |    |     |   |   __/',
-        ' \\__| _|    \\__,_| \\___|',
-        ''
-      ]
-
-      ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected)
-    })
-
-    it('does not apply DSL if wrap is false', () => {
-      const ui = cliui({
-        width: 40,
-        wrap: false
-      })
-
-      ui.div(
-        'Usage: $0\ttwo\tthree'
-      )
-
-      ui.toString().should.eql('Usage: $0\ttwo\tthree')
-    })
-  })
-})
diff --git a/test/deno/cliui-test.ts b/test/deno/cliui-test.ts
index ce24068d..cae566ff 100644
--- a/test/deno/cliui-test.ts
+++ b/test/deno/cliui-test.ts
@@ -36,13 +36,12 @@ Deno.test('evenly divides text across columns if multiple columns are given', ()
   })
 
   // it should wrap each column appropriately.
-  // TODO: we should flesh out the Deno and ESM implementation
-  // such that it spreads words out over multiple columns appropriately:
   const expected = [
-    'i am a string ti am a seconi am a third',
-    'hat should be wd string tha string that',
-    'rapped         t should be  should be w',
-    '               wrapped     rapped'
+    'i am a string  i am a      i am a third',
+    'that should be second      string that',
+    'wrapped        string that should be',
+    '               should be   wrapped',
+    '               wrapped'
   ]
 
   ui.toString().split('\n').forEach((line: string, i: number) => {
diff --git a/test/esm/cliui-test.mjs b/test/esm/cliui-test.mjs
index f57d77d1..b740be39 100644
--- a/test/esm/cliui-test.mjs
+++ b/test/esm/cliui-test.mjs
@@ -1,47 +1,4 @@
-import {ok as assert, strictEqual} from 'assert'
+import { runTests } from '../shared-tests.cjs'
 import cliui from '../../index.mjs'
 
-describe('ESM', () => {
-  it("wraps text at 'width' if a single column is given", () => {
-    const ui = cliui({
-      width: 10
-    })
-
-    ui.div('i am a string that should be wrapped')
-
-    ui.toString().split('\n').forEach((row) => {
-      assert(row.length <= 10)
-    })
-  })
-
-  it('evenly divides text across columns if multiple columns are given', () => {
-    const ui = cliui({
-      width: 40
-    })
-
-    ui.div(
-      { text: 'i am a string that should be wrapped', width: 15 },
-      'i am a second string that should be wrapped',
-      'i am a third string that should be wrapped'
-    )
-
-    // total width of all columns is <=
-    // the width cliui is initialized with.
-    ui.toString().split('\n').forEach((row) => {
-      assert(row.length <= 40)
-    })
-
-    // it should wrap each column appropriately.
-    // TODO: we should flesh out the Deno and ESM implementation
-    // such that it spreads words out over multiple columns appropriately:
-    const expected = [
-      'i am a string ti am a seconi am a third',
-      'hat should be wd string tha string that',
-      'rapped         t should be  should be w',
-      '               wrapped     rapped'
-    ]
-    ui.toString().split('\n').forEach((line, i) => {
-      strictEqual(line, expected[i])
-    })
-  })
-})
\ No newline at end of file
+runTests(cliui, 'ESM')
diff --git a/test/shared-tests.cjs b/test/shared-tests.cjs
new file mode 100644
index 00000000..86ba98c6
--- /dev/null
+++ b/test/shared-tests.cjs
@@ -0,0 +1,508 @@
+'use strict'
+
+/* global describe, it */
+
+// These are the full tests originally used only for cjs, now also run from esm.
+// (Not attempting to run from deno.)
+
+require('chai').should()
+
+// stripAnsi adapted from:
+// The npm application
+// Copyright (c) npm, Inc. and Contributors
+// Licensed on the terms of The Artistic License 2.0
+// See: https://github.com/npm/cli/blob/4c65cd952bc8627811735bea76b9b110cc4fc80e/lib/utils/ansi-trim.js
+const ansi = new RegExp('\x1b(?:\\[(?:\\d+[ABCDEFGJKSTm]|\\d+;\\d+[Hfm]|' +
+'\\d+;\\d+;\\d+m|6n|s|u|\\?25[lh])|\\w)', 'g')
+function stripAnsi (str) {
+  return str.replace(ansi, '')
+}
+
+// Force chalk to enable color, if it's disabled the test fails.
+process.env.FORCE_COLOR = 1
+
+const chalk = require('chalk')
+
+function runTests (cliui, moduleType) {
+  describe(moduleType, () => {
+    describe('resetOutput', () => {
+      it('should set lines to empty', () => {
+        const ui = cliui()
+        ui.div('i am a value that would be in a line')
+        ui.resetOutput()
+        ui.toString().length.should.be.equal(0)
+      })
+    })
+
+    describe('div', () => {
+      it("wraps text at 'width' if a single column is given", () => {
+        const ui = cliui({
+          width: 10
+        })
+
+        ui.div('i am a string that should be wrapped')
+
+        ui.toString().split('\n').forEach(row => {
+          row.length.should.be.lte(10)
+        })
+      })
+
+      it('evenly divides text across columns if multiple columns are given', () => {
+        const ui = cliui({
+          width: 40
+        })
+
+        ui.div(
+          { text: 'i am a string that should be wrapped', width: 15 },
+          'i am a second string that should be wrapped',
+          'i am a third string that should be wrapped'
+        )
+
+        // total width of all columns is <=
+        // the width cliui is initialized with.
+        ui.toString().split('\n').forEach(row => {
+          row.length.should.be.lte(40)
+        })
+
+        // it should wrap each column appropriately.
+        const expected = [
+          'i am a string  i am a      i am a third',
+          'that should be second      string that',
+          'wrapped        string that should be',
+          '               should be   wrapped',
+          '               wrapped'
+        ]
+
+        ui.toString().split('\n').should.eql(expected)
+      })
+
+      it('allows for a blank row to be appended', () => {
+        const ui = cliui({
+          width: 40
+        })
+
+        ui.div()
+
+        // it should wrap each column appropriately.
+        const expected = ['']
+
+        ui.toString().split('\n').should.eql(expected)
+      })
+    })
+
+    describe('_columnWidths', () => {
+      it('uses same width for each column by default', () => {
+        const ui = cliui({
+          width: 40
+        })
+        const widths = ui.columnWidths([{}, {}, {}])
+
+        widths[0].should.equal(13)
+        widths[1].should.equal(13)
+        widths[2].should.equal(13)
+      })
+
+      it('divides width over remaining columns if first column has width specified', () => {
+        const ui = cliui({
+          width: 40
+        })
+        const widths = ui.columnWidths([{ width: 20 }, {}, {}])
+
+        widths[0].should.equal(20)
+        widths[1].should.equal(10)
+        widths[2].should.equal(10)
+      })
+
+      it('divides width over remaining columns if middle column has width specified', () => {
+        const ui = cliui({
+          width: 40
+        })
+        const widths = ui.columnWidths([{}, { width: 10 }, {}])
+
+        widths[0].should.equal(15)
+        widths[1].should.equal(10)
+        widths[2].should.equal(15)
+      })
+
+      it('keeps track of remaining width if multiple columns have width specified', () => {
+        const ui = cliui({
+          width: 40
+        })
+        const widths = ui.columnWidths([{ width: 20 }, { width: 12 }, {}])
+
+        widths[0].should.equal(20)
+        widths[1].should.equal(12)
+        widths[2].should.equal(8)
+      })
+
+      it('uses a sane default if impossible widths are specified', () => {
+        const ui = cliui({
+          width: 40
+        })
+        const widths = ui.columnWidths([{ width: 30 }, { width: 30 }, { padding: [0, 2, 0, 1] }])
+
+        widths[0].should.equal(30)
+        widths[1].should.equal(30)
+        widths[2].should.equal(4)
+      })
+    })
+
+    describe('alignment', () => {
+      it('allows a column to be right aligned', () => {
+        const ui = cliui({
+          width: 40
+        })
+
+        ui.div(
+          'i am a string',
+          { text: 'i am a second string', align: 'right' },
+          'i am a third string that should be wrapped'
+        )
+
+        // it should right-align the second column.
+        const expected = [
+          'i am a stringi am a secondi am a third',
+          '                    stringstring that',
+          '                          should be',
+          '                          wrapped'
+        ]
+
+        ui.toString().split('\n').should.eql(expected)
+      })
+
+      it('allows a column to be center aligned', () => {
+        const ui = cliui({
+          width: 60
+        })
+
+        ui.div(
+          'i am a string',
+          { text: 'i am a second string', align: 'center', padding: [0, 2, 0, 2] },
+          'i am a third string that should be wrapped'
+        )
+
+        // it should right-align the second column.
+        const expected = [
+          'i am a string          i am a second       i am a third string',
+          '                           string          that should be',
+          '                                           wrapped'
+        ]
+
+        ui.toString().split('\n').should.eql(expected)
+      })
+    })
+
+    describe('padding', () => {
+      it('handles left/right padding', () => {
+        const ui = cliui({
+          width: 40
+        })
+
+        ui.div(
+          { text: 'i have padding on my left', padding: [0, 0, 0, 4] },
+          { text: 'i have padding on my right', padding: [0, 2, 0, 0], align: 'center' },
+          { text: 'i have no padding', padding: [0, 0, 0, 0] }
+        )
+
+        // it should add left/right padding to columns.
+        const expected = [
+          '    i have     i have      i have no',
+          '    padding  padding on    padding',
+          '    on my     my right',
+          '    left'
+        ]
+
+        ui.toString().split('\n').should.eql(expected)
+      })
+
+      it('handles top/bottom padding', () => {
+        const ui = cliui({
+          width: 40
+        })
+
+        ui.div(
+          'i am a string',
+          { text: 'i am a second string', padding: [2, 0, 0, 0] },
+          { text: 'i am a third string that should be wrapped', padding: [0, 0, 1, 0] }
+        )
+
+        // it should add top/bottom padding to second
+        // and third columns.
+        const expected = [
+          'i am a string             i am a third',
+          '                          string that',
+          '             i am a secondshould be',
+          '             string       wrapped',
+          ''
+        ]
+
+        ui.toString().split('\n').should.eql(expected)
+      })
+
+      it('preserves leading whitespace as padding', () => {
+        const ui = cliui()
+
+        ui.div('     LEADING WHITESPACE')
+        ui.div('\u001b[34m     with ansi\u001b[39m')
+
+        const expected = [
+          '     LEADING WHITESPACE',
+          '     with ansi'
+        ]
+
+        ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected)
+      })
+    })
+
+    describe('border', () => {
+      it('allows a border to be placed around a div', () => {
+        const ui = cliui({
+          width: 40
+        })
+
+        ui.div(
+          { text: 'i am a first string', padding: [0, 0, 0, 0], border: true },
+          { text: 'i am a second string', padding: [1, 0, 0, 0], border: true }
+        )
+
+        const expected = [
+          '.------------------.',
+          '| i am a first     |.------------------.',
+          '| string           || i am a second    |',
+          "'------------------'| string           |",
+          "                    '------------------'"
+        ]
+
+        ui.toString().split('\n').should.eql(expected)
+      })
+    })
+
+    describe('wrap', () => {
+      it('allows wordwrap to be disabled', () => {
+        const ui = cliui({
+          wrap: false
+        })
+
+        ui.div(
+          { text: 'i am a string', padding: [0, 1, 0, 0] },
+          { text: 'i am a second string', padding: [0, 2, 0, 0] },
+          { text: 'i am a third string that should not be wrapped', padding: [0, 0, 0, 2] }
+        )
+
+        ui.toString().should.equal('i am a string i am a second string    i am a third string that should not be wrapped')
+      })
+    })
+
+    describe('span', () => {
+      it('appends the next row to the end of the prior row if it fits', () => {
+        const ui = cliui({
+          width: 40
+        })
+
+        ui.span(
+          { text: 'i am a string that will be wrapped', width: 30 }
+        )
+
+        ui.div(
+          { text: ' [required] [default: 99]', align: 'right' }
+        )
+
+        const expected = [
+          'i am a string that will be',
+          'wrapped         [required] [default: 99]'
+        ]
+
+        ui.toString().split('\n').should.eql(expected)
+      })
+
+      it('does not append the string if it does not fit on the prior row', () => {
+        const ui = cliui({
+          width: 40
+        })
+
+        ui.span(
+          { text: 'i am a string that will be wrapped', width: 30 }
+        )
+
+        ui.div(
+          { text: 'i am a second row', align: 'left' }
+        )
+
+        const expected = [
+          'i am a string that will be',
+          'wrapped',
+          'i am a second row'
+        ]
+
+        ui.toString().split('\n').should.eql(expected)
+      })
+
+      it('always appends text to prior span if wrap is disabled', () => {
+        const ui = cliui({
+          wrap: false,
+          width: 40
+        })
+
+        ui.span(
+          { text: 'i am a string that will be wrapped', width: 30 }
+        )
+
+        ui.div(
+          { text: 'i am a second row', align: 'left', padding: [0, 0, 0, 3] }
+        )
+
+        ui.div('a third line')
+
+        const expected = [
+          'i am a string that will be wrapped   i am a second row',
+          'a third line'
+        ]
+
+        ui.toString().split('\n').should.eql(expected)
+      })
+
+      it('appends to prior line appropriately when strings contain ansi escape codes', () => {
+        const ui = cliui({
+          width: 40
+        })
+
+        ui.span(
+          { text: chalk.green('i am a string that will be wrapped'), width: 30 }
+        )
+
+        ui.div(
+          { text: chalk.blue(' [required] [default: 99]'), align: 'right' }
+        )
+
+        const expected = [
+          'i am a string that will be',
+          'wrapped         [required] [default: 99]'
+        ]
+
+        ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected)
+      })
+    })
+
+    describe('layoutDSL', () => {
+      it('turns tab into multiple columns', () => {
+        const ui = cliui({
+          width: 60
+        })
+
+        ui.div(
+          '  <regex>  \tmy awesome regex\n  <my second thing>  \tanother row\t  a third column'
+        )
+
+        const expected = [
+          '  <regex>            my awesome regex',
+          '  <my second thing>  another row          a third column'
+        ]
+
+        ui.toString().split('\n').should.eql(expected)
+      })
+
+      it('turns newline into multiple rows', () => {
+        const ui = cliui({
+          width: 40
+        })
+
+        ui.div(
+          'Usage: $0\n  <regex>\t  my awesome regex\n  <glob>\t  my awesome glob\t  [required]'
+        )
+        const expected = [
+          'Usage: $0',
+          '  <regex>  my awesome regex',
+          '  <glob>   my awesome     [required]',
+          '           glob'
+        ]
+
+        ui.toString().split('\n').should.eql(expected)
+      })
+
+      it('aligns rows appropriately when they contain ansi escape codes', () => {
+        const ui = cliui({
+          width: 40
+        })
+
+        ui.div(
+          '  <regex>\t  ' + chalk.red('my awesome regex') + '\t  [regex]\n  ' + chalk.blue('<glob>') + '\t  my awesome glob\t  [required]'
+        )
+
+        const expected = [
+          '  <regex>  my awesome     [regex]',
+          '           regex',
+          '  <glob>   my awesome     [required]',
+          '           glob'
+        ]
+
+        ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected)
+      })
+
+      it('ignores ansi escape codes when measuring padding', () => {
+        // Forcefully enable color-codes for this test
+        const { enabled, level } = chalk
+        chalk.enabled = true
+        chalk.level = 1
+
+        const ui = cliui({
+          width: 25
+        })
+
+        // using figlet font 'Shadow' rendering of text 'true' here
+        ui.div(
+          chalk.blue('  |                      \n  __|   __|  |   |   _ \\ \n  |    |     |   |   __/ \n \\__| _|    \\__,_| \\___| \n                         ')
+        )
+
+        // relevant part is first line - leading whitespace should be preserved as left padding
+        const expected = [
+          '  |',
+          '  __|   __|  |   |   _ \\',
+          '  |    |     |   |   __/',
+          ' \\__| _|    \\__,_| \\___|',
+          '                         '
+        ]
+
+        ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected)
+        chalk.enabled = enabled
+        chalk.level = level
+      })
+
+      it('correctly handles lack of ansi escape codes when measuring padding', () => {
+        const ui = cliui({
+          width: 25
+        })
+
+        // using figlet font 'Shadow' rendering of text 'true' here
+        ui.div(
+          '  |                      \n  __|   __|  |   |   _ \\ \n  |    |     |   |   __/ \n \\__| _|    \\__,_| \\___| \n                         '
+        )
+
+        // The difference
+        const expected = [
+          '  |',
+          '  __|   __|  |   |   _ \\',
+          '  |    |     |   |   __/',
+          ' \\__| _|    \\__,_| \\___|',
+          ''
+        ]
+
+        ui.toString().split('\n').map(l => stripAnsi(l)).should.eql(expected)
+      })
+
+      it('does not apply DSL if wrap is false', () => {
+        const ui = cliui({
+          width: 40,
+          wrap: false
+        })
+
+        ui.div(
+          'Usage: $0\ttwo\tthree'
+        )
+
+        ui.toString().should.eql('Usage: $0\ttwo\tthree')
+      })
+    })
+  })
+}
+
+module.exports = { runTests }
