├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── bench ├── index.js ├── math.js ├── package.json └── suites │ ├── ava │ └── index.spec.js │ ├── jest │ └── index.spec.js │ ├── mocha │ └── index.js │ ├── tape │ └── index.js │ └── uvu │ └── index.js ├── bin.js ├── docs ├── api.assert.md ├── api.uvu.md ├── cli.md ├── coverage.md ├── esm.md └── watch.md ├── examples ├── basic │ ├── package.json │ ├── src │ │ ├── math.js │ │ └── utils.js │ └── tests │ │ ├── math.js │ │ └── utils.js ├── coverage │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── math.js │ │ └── utils.js │ └── tests │ │ ├── math.js │ │ └── utils.js ├── esbuild │ ├── package.json │ ├── src │ │ ├── math.ts │ │ └── utils.ts │ ├── tests │ │ ├── math.ts │ │ └── utils.ts │ └── tsconfig.json ├── esm.dual │ ├── package.json │ ├── readme.md │ ├── src │ │ ├── math.js │ │ └── utils.js │ └── tests │ │ ├── math.js │ │ └── utils.js ├── esm.loader │ ├── package.json │ ├── readme.md │ ├── src │ │ ├── math.js │ │ └── utils.js │ └── tests │ │ ├── math.js │ │ └── utils.js ├── esm.mjs │ ├── package.json │ ├── readme.md │ ├── src │ │ ├── math.mjs │ │ └── utils.mjs │ └── tests │ │ ├── math.mjs │ │ └── utils.mjs ├── monorepo │ ├── package.json │ └── packages │ │ ├── math │ │ ├── package.json │ │ ├── src │ │ │ └── index.js │ │ └── tests │ │ │ └── index.js │ │ └── utils │ │ ├── package.json │ │ ├── src │ │ └── index.js │ │ └── tests │ │ └── index.js ├── preact │ ├── .babelrc.json │ ├── package.json │ ├── src │ │ └── Count.jsx │ └── tests │ │ ├── Count.js │ │ └── setup │ │ └── env.js ├── puppeteer │ ├── package.json │ ├── readme.md │ ├── src │ │ ├── app.js │ │ ├── index.html │ │ ├── math.js │ │ └── utils.js │ └── tests │ │ ├── index.js │ │ ├── math.js │ │ ├── setup │ │ └── puppeteer.js │ │ └── utils.js ├── solidjs │ ├── package.json │ ├── src │ │ └── Count.tsx │ └── tests │ │ └── Count.test.tsx ├── suites │ ├── package.json │ ├── src │ │ ├── math.js │ │ └── utils.js │ └── tests │ │ ├── math.js │ │ └── utils.js ├── supertest │ ├── package.json │ ├── src │ │ ├── index.js │ │ ├── pets.js │ │ └── users.js │ └── tests │ │ ├── index.js │ │ ├── pets.js │ │ └── users.js ├── svelte │ ├── package.json │ ├── src │ │ └── Count.svelte │ └── tests │ │ ├── Count.js │ │ └── setup │ │ ├── env.js │ │ └── register.js ├── typescript.module │ ├── .gitignore │ ├── loadr.mjs │ ├── package.json │ ├── src │ │ ├── math.js │ │ └── utils.js │ ├── tests │ │ ├── math.ts │ │ └── utils.ts │ └── tsconfig.json ├── typescript │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── math.ts │ │ └── utils.ts │ ├── tests │ │ ├── math.ts │ │ └── utils.ts │ └── tsconfig.json └── watch │ ├── package.json │ ├── src │ ├── math.js │ └── utils.js │ └── tests │ ├── math.js │ └── utils.js ├── license ├── package.json ├── parse ├── index.d.ts ├── index.js └── index.mjs ├── readme.md ├── run ├── index.d.ts ├── index.js └── index.mjs ├── shots ├── suites.gif └── uvu.jpg ├── src ├── assert.d.ts ├── assert.js ├── diff.d.ts ├── diff.js ├── index.d.ts └── index.js └── test ├── assert.js ├── diff.js ├── exit.fails.js ├── index.js ├── parse.js ├── suite.js └── uvu.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = tab 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{json,yml,md}] 13 | indent_style = space 14 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: lukeed 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'docs/**' 7 | - 'bench/**' 8 | - 'examples/**' 9 | - 'shots/**' 10 | branches: 11 | - master 12 | pull_request: 13 | paths-ignore: 14 | - 'docs/**' 15 | - 'bench/**' 16 | - 'examples/**' 17 | - 'shots/**' 18 | branches: 19 | - master 20 | 21 | jobs: 22 | test: 23 | name: Node.js v${{ matrix.nodejs }} (${{ matrix.os }}) 24 | runs-on: ${{ matrix.os }} 25 | timeout-minutes: 3 26 | strategy: 27 | matrix: 28 | nodejs: [8, 10, 12, 14, 16] 29 | os: [ubuntu-latest, windows-latest, macOS-latest] 30 | steps: 31 | - uses: actions/checkout@v2 32 | - uses: actions/setup-node@v2 33 | with: 34 | node-version: ${{ matrix.nodejs }} 35 | 36 | - name: Install 37 | run: | 38 | npm install 39 | npm install -g nyc@13 40 | 41 | - name: Test w/ Coverage 42 | run: nyc --include=src npm test 43 | 44 | - name: Report 45 | if: matrix.nodejs >= 16 && matrix.os == 'ubuntu-latest' 46 | run: | 47 | nyc report --reporter=text-lcov > coverage.lcov 48 | bash <(curl -s https://codecov.io/bash) 49 | env: 50 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *-lock.* 4 | *.lock 5 | *.log 6 | 7 | /*.d.ts 8 | 9 | /dist 10 | /assert 11 | /diff 12 | -------------------------------------------------------------------------------- /bench/index.js: -------------------------------------------------------------------------------- 1 | const { reset } = require('kleur'); 2 | const { promisify } = require('util'); 3 | const { execFile } = require('child_process'); 4 | 5 | const spawn = promisify(execFile); 6 | const PAD = reset().dim(' || '); 7 | 8 | const runners = { 9 | ava: [require.resolve('ava/cli.js'), 'suites/ava/**'], 10 | jest: [require.resolve('jest/bin/jest.js'), 'suites/jest', '--env=node'], 11 | mocha: [require.resolve('mocha/bin/mocha'), 'suites/mocha'], 12 | tape: [require.resolve('tape/bin/tape'), 'suites/tape'], 13 | uvu: [require.resolve('uvu/bin.js'), 'suites/uvu'], 14 | }; 15 | 16 | function format(arr) { 17 | let num = Math.round(arr[1] / 1e6); 18 | if (arr[0] > 0) return (arr[0] + num / 1e3).toFixed(2) + 's'; 19 | return `${num}ms`; 20 | } 21 | 22 | async function run(name, args) { 23 | let timer = process.hrtime(); 24 | let pid = await spawn('node', args, { cwd: __dirname }); 25 | let delta = process.hrtime(timer); 26 | 27 | console.log('~> "%s" took %s', name, format(delta)); 28 | console.log(PAD + '\n' + PAD + (pid.stderr || pid.stdout).toString().replace(/(\r?\n)/g, '$1' + PAD)); 29 | } 30 | 31 | (async function () { 32 | for (let name of Object.keys(runners)) { 33 | await run(name, runners[name]); 34 | } 35 | })().catch(err => { 36 | console.error('Oops~!', err); 37 | process.exit(1); 38 | }); 39 | -------------------------------------------------------------------------------- /bench/math.js: -------------------------------------------------------------------------------- 1 | exports.sum = (a, b) => a + b; 2 | exports.div = (a, b) => a / b; 3 | exports.mod = (a, b) => a % b; 4 | -------------------------------------------------------------------------------- /bench/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "ava": "3.10.1", 5 | "jest": "26.1.0", 6 | "mocha": "8.0.1", 7 | "tape": "5.0.1", 8 | "uvu": "0.1.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /bench/suites/ava/index.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const math = require('../../math'); 3 | 4 | test('sum', t => { 5 | t.is(typeof math.sum, 'function'); 6 | t.is(math.sum(1, 2), 3); 7 | t.is(math.sum(-1, -2), -3); 8 | t.is(math.sum(-1, 1), 0); 9 | }); 10 | 11 | test('div', t => { 12 | t.is(typeof math.div, 'function'); 13 | t.is(math.div(1, 2), 0.5); 14 | t.is(math.div(-1, -2), 0.5); 15 | t.is(math.div(-1, 1), -1); 16 | }); 17 | 18 | test('mod', t => { 19 | t.is(typeof math.mod, 'function'); 20 | t.is(math.mod(1, 2), 1); 21 | t.is(math.mod(-3, -2), -1); 22 | t.is(math.mod(7, 4), 3); 23 | }); 24 | -------------------------------------------------------------------------------- /bench/suites/jest/index.spec.js: -------------------------------------------------------------------------------- 1 | const math = require('../../math'); 2 | 3 | describe('sum', () => { 4 | it('should be a function', () => { 5 | expect(typeof math.sum).toBe('function'); 6 | }); 7 | 8 | it('should compute values correctly', () => { 9 | expect(math.sum(1, 2)).toBe(3); 10 | expect(math.sum(-1, -2)).toBe(-3); 11 | expect(math.sum(-1, 1)).toBe(0); 12 | }); 13 | }); 14 | 15 | describe('div', () => { 16 | it('should be a function', () => { 17 | expect(typeof math.div).toBe('function'); 18 | }); 19 | 20 | it('should compute values correctly', () => { 21 | expect(math.div(1, 2)).toBe(0.5); 22 | expect(math.div(-1, -2)).toBe(0.5); 23 | expect(math.div(-1, 1)).toBe(-1); 24 | }); 25 | }); 26 | 27 | describe('mod', () => { 28 | it('should be a function', () => { 29 | expect(typeof math.mod).toBe('function'); 30 | }); 31 | 32 | it('should compute values correctly', () => { 33 | expect(math.mod(1, 2)).toBe(1); 34 | expect(math.mod(-3, -2)).toBe(-1); 35 | expect(math.mod(7, 4)).toBe(3); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /bench/suites/mocha/index.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const math = require('../../math'); 3 | 4 | describe('sum', () => { 5 | it('should be a function', () => { 6 | assert.equal(typeof math.sum, 'function'); 7 | }); 8 | 9 | it('should compute result correctly', () => { 10 | assert.equal(math.sum(1, 2), 3); 11 | assert.equal(math.sum(-1, -2), -3); 12 | assert.equal(math.sum(-1, 1), 0); 13 | }); 14 | }); 15 | 16 | describe('div', () => { 17 | it('should be a function', () => { 18 | assert.equal(typeof math.div, 'function'); 19 | }); 20 | 21 | it('should compute result correctly', () => { 22 | assert.equal(math.div(1, 2), 0.5); 23 | assert.equal(math.div(-1, -2), 0.5); 24 | assert.equal(math.div(-1, 1), -1); 25 | }); 26 | }); 27 | 28 | describe('mod', () => { 29 | it('should be a function', () => { 30 | assert.equal(typeof math.mod, 'function'); 31 | }); 32 | 33 | it('should compute result correctly', () => { 34 | assert.equal(math.mod(1, 2), 1); 35 | assert.equal(math.mod(-3, -2), -1); 36 | assert.equal(math.mod(7, 4), 3); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /bench/suites/tape/index.js: -------------------------------------------------------------------------------- 1 | const test = require('tape'); 2 | const math = require('../../math'); 3 | 4 | test('sum', t => { 5 | t.is(typeof math.sum, 'function'); 6 | t.is(math.sum(1, 2), 3); 7 | t.is(math.sum(-1, -2), -3); 8 | t.is(math.sum(-1, 1), 0); 9 | t.end(); 10 | }); 11 | 12 | test('div', t => { 13 | t.is(typeof math.div, 'function'); 14 | t.is(math.div(1, 2), 0.5); 15 | t.is(math.div(-1, -2), 0.5); 16 | t.is(math.div(-1, 1), -1); 17 | t.end(); 18 | }); 19 | 20 | test('mod', t => { 21 | t.is(typeof math.mod, 'function'); 22 | t.is(math.mod(1, 2), 1); 23 | t.is(math.mod(-3, -2), -1); 24 | t.is(math.mod(7, 4), 3); 25 | t.end(); 26 | }); 27 | -------------------------------------------------------------------------------- /bench/suites/uvu/index.js: -------------------------------------------------------------------------------- 1 | const { test } = require('uvu'); 2 | const assert = require('uvu/assert'); 3 | const math = require('../../math'); 4 | 5 | test('sum', () => { 6 | assert.type(math.sum, 'function'); 7 | assert.is(math.sum(1, 2), 3); 8 | assert.is(math.sum(-1, -2), -3); 9 | assert.is(math.sum(-1, 1), 0); 10 | }); 11 | 12 | test('div', () => { 13 | assert.type(math.div, 'function'); 14 | assert.is(math.div(1, 2), 0.5); 15 | assert.is(math.div(-1, -2), 0.5); 16 | assert.is(math.div(-1, 1), -1); 17 | }); 18 | 19 | test('mod', () => { 20 | assert.type(math.mod, 'function'); 21 | assert.is(math.mod(1, 2), 1); 22 | assert.is(math.mod(-3, -2), -1); 23 | assert.is(math.mod(7, 4), 3); 24 | }); 25 | 26 | test.run(); 27 | -------------------------------------------------------------------------------- /bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const sade = require('sade'); 3 | const pkg = require('./package'); 4 | const { parse } = require('./parse'); 5 | 6 | const dimport = x => new Function(`return import(${ JSON.stringify(x) })`).call(0); 7 | 8 | const hasImport = (() => { 9 | try { new Function('import').call(0) } 10 | catch (err) { return !/unexpected/i.test(err.message) } 11 | })(); 12 | 13 | sade('uvu [dir] [pattern]') 14 | .version(pkg.version) 15 | .option('-b, --bail', 'Exit on first failure') 16 | .option('-i, --ignore', 'Any file patterns to ignore') 17 | .option('-r, --require', 'Additional module(s) to preload') 18 | .option('-C, --cwd', 'The current directory to resolve from', '.') 19 | .option('-c, --color', 'Print colorized output', true) 20 | .action(async (dir, pattern, opts) => { 21 | try { 22 | if (opts.color) process.env.FORCE_COLOR = '1'; 23 | let ctx = await parse(dir, pattern, opts); 24 | 25 | if (!ctx.requires && hasImport) { 26 | await dimport('uvu/run').then(m => m.run(ctx.suites, opts)); 27 | } else { 28 | await require('uvu/run').run(ctx.suites, opts); 29 | } 30 | } catch (err) { 31 | console.error(err.stack || err.message); 32 | process.exit(1); 33 | } 34 | }) 35 | .parse(process.argv); 36 | -------------------------------------------------------------------------------- /docs/api.assert.md: -------------------------------------------------------------------------------- 1 | # `uvu/assert` 2 | 3 | The `uvu/assert` module is a collection of assertion methods that, like `uvu` itself, work in Node.js _and_ browser contexts. Additionally, `uvu/assert` is _completely_ optional, allowing you to bring along existing favorites. 4 | 5 | Because `uvu` operates through thrown `Error`s (or lack thereof), any `Error`-based utility can be used as an assertion. As a basic example, this is a completely valid `uvu` test: 6 | 7 | ```js 8 | import { test } from 'uvu'; 9 | 10 | test('this will fail', () => { 11 | if (1 !== 2) throw new Error('Duh!'); 12 | }); 13 | 14 | test.run(); 15 | ``` 16 | 17 | With this, `uvu` will register that the `"this will fail"` test failed.
You will only be missing the detailed insights (aka, pretty diff'ing) that the included [`Assertion`](#assertionoptions) errors provide. 18 | 19 | ## API 20 | 21 | > For all API methods listed:
22 | > * `T` represents any data type
23 | > * `Message` can be a string (for custom assertion message) or an `Error` instance 24 | 25 | ### ok(actual: T, msg?: Message) 26 | Assert that `actual` is a truthy value. 27 | 28 | ```js 29 | assert.ok(12345); 30 | assert.ok(!false); 31 | assert.ok('hello'); 32 | ``` 33 | 34 | ### is(actual: T, expects: T, msg?: Message) 35 | Assert that `actual` strictly equals (`===`) the `expects` value. 36 | 37 | ```js 38 | assert.is('hello', 'hello'); 39 | 40 | const arr = [1, 2, 3]; 41 | assert.is(arr, [1, 2, 3]); //=> fails 42 | assert.is(arr, arr); //=> pass 43 | ``` 44 | 45 | ### is.not(actual: T, expects: T, msg?: Message) 46 | Assert that `actual` does _not_ strictly equal (`===`) the `expects` value. 47 | 48 | ```js 49 | assert.is.not(123, '123'); 50 | assert.is.not(true, false); 51 | ``` 52 | 53 | ### equal(actual: T, expects: T, msg?: Message) 54 | Assert that `actual` is deeply equal to the `expects` value.
Visit [`dequal`](https://github.com/lukeed/dequal) for more information. 55 | 56 | ```js 57 | const input = { 58 | foo: 123, 59 | bar: [4, 5, 6] 60 | }; 61 | 62 | assert.equal(input, { 63 | foo: 123, 64 | bar: [4, 5, 6] 65 | }); 66 | ``` 67 | 68 | ### type(actual: T, expects: Types, msg?: Message) 69 | Assert that `typeof actual` is equal to the `expects` type.
Available `Types` are: `string`, `number`, `boolean`, `object`, `undefined`, and `function`. 70 | 71 | ```js 72 | assert.type(123, 'number'); 73 | assert.type('hello', 'string'); 74 | assert.type(assert.type, 'function'); 75 | ``` 76 | 77 | ### instance(actual: T, expects: T, msg?: Message) 78 | Assert that `actual` is an `instanceof` the `expects` constructor. 79 | 80 | ```js 81 | assert.instance(new Date, Date); 82 | assert.instance([1, 2, 3], Array); 83 | assert.instance(/foobar/gi, RegExp); 84 | ``` 85 | 86 | ### match(actual: string, expects: RegExp | String, msg?: Message) 87 | Assert that `actual` matches the `expects` pattern. 88 | 89 | When `expects` is a regular expression, it must match the `actual` value. 90 | When `expects` is a string, it must exist within the `actual` value as a substring. 91 | 92 | ```js 93 | assert.match('hello world', 'wor'); 94 | assert.match('hello world', /^hel/); 95 | ``` 96 | 97 | ### snapshot(actual: string, expects: string, msg?: Message) 98 | Assert that `actual` matches the `expects` multi-line string. 99 | 100 | ```js 101 | assert.snapshot( 102 | JSON.stringify({ foo: 123 }, null, 2), 103 | `{\n "foo": 123\n}` 104 | ); 105 | ``` 106 | 107 | ### fixture(actual: string, expects: string, msg?: Message) 108 | Assert that `actual` matches the `expects` multi-line string.
Equivalent to `assert.snapshot` except that line numbers are printed in the error diff! 109 | 110 | ```js 111 | assert.fixture( 112 | JSON.stringify({ foo: 123, bar: 456 }, null, 2), 113 | fs.readFileSync('fixture.json', 'utf8') 114 | ); 115 | ``` 116 | 117 | ### throws(fn: Function, expects?: Message | RegExp | Function, msg?: Message) 118 | Assert that the `fn` function throws an Error. 119 | 120 | When `expects` is not defined, then _any_ Error thrown satisfies the assertion.
121 | When `expects` is a string, then the `Error`'s message must contain the `expects` string.
122 | When `expects` is a function, then `expects` will receive the thrown `Error` and must return a `boolean` determination. 123 | 124 | Since `expects` is optional, you may also invoke the `assert.throws(fn, msg)` signature. 125 | 126 | ```js 127 | const OOPS = () => (null)[0]; 128 | 129 | assert.throws(() => OOPS()); 130 | assert.throws(() => OOPS(), /Cannot read property/); 131 | assert.throws(() => OOPS(), err => err instanceof TypeError); 132 | ``` 133 | 134 | If you are trying to assert that an `async` function throws an Error, the following approach [is recommended](https://github.com/lukeed/uvu/issues/35#issuecomment-896270152): 135 | 136 | ```js 137 | try { 138 | await asyncFnThatThrows(); 139 | assert.unreachable('should have thrown'); 140 | } catch (err) { 141 | assert.instance(err, Error); 142 | assert.match(err.message, 'something specific'); 143 | assert.is(err.code, 'ERROR123'); 144 | } 145 | ``` 146 | 147 | ### unreachable(msg?: Message) 148 | Assert that a line should never be reached. 149 | 150 | ```js 151 | try { 152 | throw new Error('Oops'); 153 | assert.unreachable('I will not run'); 154 | } catch (err) { 155 | assert.is(err.message, 'Oops'); 156 | } 157 | ``` 158 | 159 | ### not(actual: T, msg?: Message) 160 | Assert that `actual` is falsey. 161 | 162 | ```js 163 | assert.not(0); 164 | assert.not(null); 165 | assert.not(false); 166 | ``` 167 | 168 | ### not.ok(actual: T, msg?: Message) 169 | Assert that `actual` is not truthy.
This is an alias for `assert.not`. 170 | 171 | ### not.equal(actual: T, expects: T, msg?: Message) 172 | Assert that `actual` does not deeply equal the `expects` value.
Visit [`dequal`](https://github.com/lukeed/dequal) for more information. 173 | 174 | ```js 175 | const input = { 176 | foo: 123, 177 | bar: [4, 5, 6] 178 | }; 179 | 180 | assert.not.equal(input, { 181 | foo: 123 182 | }); 183 | ``` 184 | 185 | ### not.type(actual: T, expects: Types, msg?: Message) 186 | Assert that `typeof actual` is not equal to the `expects` type.
Available `Types` are: `string`, `number`, `boolean`, `object`, `undefined`, and `function`. 187 | 188 | ```js 189 | assert.not.type(123, 'object'); 190 | assert.not.type('hello', 'number'); 191 | assert.not.type(assert.type, 'undefined'); 192 | ``` 193 | 194 | ### not.instance(actual: T, expects: T, msg?: Message) 195 | Assert that `actual` is not an `instanceof` the `expects` constructor. 196 | 197 | ```js 198 | assert.not.instance(new Date, Number); 199 | assert.not.instance([1, 2, 3], String); 200 | assert.not.instance(/foobar/gi, Date); 201 | ``` 202 | 203 | ### not.match(actual: string, expects: RegExp | String, msg?: Message) 204 | Assert that `actual` does not match the `expects` pattern. 205 | 206 | When `expects` is a regular expression, it must not match the `actual` value. 207 | When `expects` is a string, it must not exist within the `actual` value as a substring. 208 | 209 | ```js 210 | assert.not.match('hello world', 'other'); 211 | assert.not.match('hello world', /other/g); 212 | ``` 213 | 214 | ### not.snapshot(actual: string, expects: string, msg?: Message) 215 | Assert that `actual` does not match the `expects` snapshot. 216 | 217 | ```js 218 | assert.not.snapshot( 219 | JSON.stringify({ foo: 123 }, null, 2), 220 | `{"foo":123,"bar":456}` 221 | ); 222 | ``` 223 | 224 | ### not.fixture(actual: string, expects: string, msg?: Message) 225 | Assert that `actual` does not match the `expects` multi-line string.
Equivalent to `assert.not.snapshot` except that line numbers are printed in the error diff! 226 | 227 | ```js 228 | assert.not.fixture( 229 | JSON.stringify({ foo: 123, bar: 456 }, null, 2), 230 | fs.readFileSync('fixture.json', 'utf8') 231 | ); 232 | ``` 233 | 234 | ### not.throws(fn: Function, expects?: Message | RegExp | Function, msg?: Message) 235 | Assert that the `fn` function does not throw, _or_ does not throw of `expects` type. 236 | 237 | ```js 238 | const PASS = () => {}; 239 | const FAIL = () => { 240 | throw new Error('Oops'); 241 | }; 242 | 243 | assert.not.throws(() => PASS()); //=> pass 244 | assert.not.throws(() => FAIL()); //=> fails 245 | assert.not.throws(() => FAIL(), /Oops/); //=> pass 246 | assert.not.throws(() => FAIL(), /foobar/); //=> fails 247 | assert.not.throws(() => FAIL(), err => err.message.length > 0); //=> pass 248 | ``` 249 | 250 | ### Assertion(options) 251 | 252 | The base `Assertion` class, which extends `Error` directly. 253 | 254 | Internally, `uvu` checks if thrown errors are `Assertion` errors as part of its formatting step. 255 | 256 | #### options.message 257 | Type: `string`
258 | Required: `true` 259 | 260 | The error message to print. 261 | 262 | > **Note:** By default, this is the generated default from each `uvu/assert` method. 263 | 264 | #### options.details 265 | Type: `string`
266 | Required: `false` 267 | 268 | 269 | The detailed diff output, as generated by `uvu/diff`. 270 | 271 | #### options.generated 272 | Type: `boolean`
273 | Required: `false` 274 | 275 | If the `options.message` was generated.
This will be `false` when an `uvu/assert` method received a custom message. 276 | 277 | #### options.operator 278 | Type: `string`
279 | Required: `true` 280 | 281 | The assertion method name. 282 | 283 | #### options.expects 284 | Type: `any`
285 | Required: `true` 286 | 287 | The expected value. 288 | 289 | #### options.actual; 290 | Type: `any`
291 | Required: `true` 292 | 293 | The actual value. 294 | -------------------------------------------------------------------------------- /docs/api.uvu.md: -------------------------------------------------------------------------------- 1 | # `uvu` 2 | 3 | This is the main module. All `uvu` tests require that either [`suite`](#uvusuitename-string-context-t) or [`test`](#uvutestname-string-callback-function) (or both) be imported. 4 | 5 | You may declare multiple [`Suites`](#suites) in the same file. This helps with organization as it group test output in a more readable fashion and allows related items to remain neighbors. 6 | 7 | You should choose `uvu.suite` if/when you'd like to leverage the additional organization.
You should choose `uvu.test` if you don't care about organization and/or are only planning on testing a single entity. 8 | 9 | There is no penalty for choosing `uvu.suite` vs `uvu.test`. In fact, `uvu.test` _is_ an unnamed [Suite](#suites)! 10 | 11 | No matter which you choose, the Suite's [`run`](#suiterun) must be called in order for it to be added to `uvu`'s queue. 12 | 13 | > **Note:** Because of this API decision, `uvu` test files can be executed with `node` directly! 14 | 15 | ## API 16 | 17 | ### uvu.suite(name: string, context?: T) 18 | Returns: [`Suite`](#suites) 19 | 20 | Creates a new `Suite` instance. 21 | 22 | Of course, you may have multiple `Suite`s in the same file.
However, you must remember to call `run()` on each suite! 23 | 24 | #### name 25 | Type: `String` 26 | 27 | The name of your suite.
This groups all console output together and will prefix the name of any failing test. 28 | 29 | #### context 30 | Type: `any`
31 | Default: `{}` 32 | 33 | The suite's initial [context](#context-1) value, if any.
This will be passed to every [hook](#hooks) and to every test block within the suite. 34 | 35 | > **Note:** Before v0.4.0, `uvu` attempted to provide read-only access within test handlers. Ever since, `context` is writable/mutable anywhere it's accessed. 36 | 37 | ### uvu.test(name: string, callback: function) 38 | Returns: `void` 39 | 40 | If you don't want to separate your tests into groups (aka, "suites") – or if you don't plan on testing more than one thing in a file – then you may want to import `test` for simplicity sake (naming is hard). 41 | 42 | > **Important:** The `test` export is just an unnamed [`Suite`](#suites) instance! 43 | 44 | #### name 45 | Type: `String` 46 | 47 | The name of your test.
Choose a descriptive name as it identifies failing tests. 48 | 49 | #### callback 50 | Type: `Function` or `Promise` 51 | 52 | The callback that contains your test code.
Your callback may be asynchronous and may `return` any value, although returned values are discarded completely and have no effect. 53 | 54 | 55 | ## Suites 56 | 57 | All `uvu` test suites share the same API and can be used in the same way. 58 | 59 | In fact, `uvu.test` is actually the result of unnamed `uvu.suite` call!
The only difference between them is how their results are grouped and displayed in your terminal window. 60 | 61 | ***API*** 62 | 63 | ### suite(name, callback) 64 | Every suite instance is callable.
This is the standard usage. 65 | 66 | ### suite.only(name, callback) 67 | For this `suite`, only run this test.
This is a shortcut for isolating one (or more) test blocks. 68 | 69 | > **Note:** You can invoke `only` on multiple tests! 70 | 71 | ### suite.skip(name, callback) 72 | Skip this test block entirely. 73 | 74 | ### suite.before(callback) 75 | Invoke the provided `callback` before this suite begins.
This is ideal for creating fixtures or setting up an environment.
Please see [Hooks](#hooks) for more information. 76 | 77 | ### suite.after(callback) 78 | Invoke the provided `callback` after this suite finishes.
This is ideal for fixture or environment cleanup.
Please see [Hooks](#hooks) for more information. 79 | 80 | ### suite.before.each(callback) 81 | Invoke the provided `callback` before each test of this suite begins.
Please see [Hooks](#hooks) for more information. 82 | 83 | ### suite.after.each(callback) 84 | Invoke the provided `callback` after each test of this suite finishes.
Please see [Hooks](#hooks) for more information. 85 | 86 | ### suite.run() 87 | Start/Add the suite to the `uvu` test queue. 88 | 89 | > **Important:** You **must** call this method in order for your suite to be run! 90 | 91 | ***Example*** 92 | 93 | > Check out [`/examples`](/examples) for a list of working demos! 94 | 95 | ```js 96 | import { suite } from 'uvu'; 97 | import * as assert from 'uvu/assert'; 98 | import * as dates from '../src/dates'; 99 | 100 | const Now = suite('Date.now()'); 101 | 102 | let _Date; 103 | Now.before(() => { 104 | let count = 0; 105 | _Date = global.Date; 106 | global.Date = { now: () => 100 + count++ }; 107 | }); 108 | 109 | Now.after(() => { 110 | global.Date = _Date; 111 | }); 112 | 113 | // this is not run (skip) 114 | Now.skip('should be a function', () => { 115 | assert.type(Date.now, 'function'); 116 | }); 117 | 118 | // this is not run (only) 119 | Now('should return a number', () => { 120 | assert.type(Date.now(), 'number'); 121 | }); 122 | 123 | // this is run (only) 124 | Now.only('should progress with time', () => { 125 | assert.is(Date.now(), 100); 126 | assert.is(Date.now(), 101); 127 | assert.is(Date.now(), 102); 128 | }); 129 | 130 | Now.run(); 131 | ``` 132 | 133 | 134 | ## Hooks 135 | 136 | Your suite can implement "hooks" that run before and/or after the entire suite, as well as before and/or after the suite's individual tests. 137 | 138 | It may be useful to use `suite.before` and `suite.after` to set up and teardown suite-level assumptions like: 139 | 140 | * environment variables 141 | * database clients and/or seed data 142 | * generating fixtures 143 | * mocks, spies, etc 144 | 145 | It may be appropriate to use `suite.before.each` and `suite.after.each` to reset parts of suite's context, or for passing values between tests. This may include — but of course, is not limited to — rolling back database transactions, restoring a mocked function, etc. 146 | 147 | > **Important:** Any `after` and `after.each` hooks will _always_ be invoked – including after failed assertions. 148 | 149 | Additionally, as of `uvu@0.3.0`, hooks receive the suite's `context` value. They are permitted to modify the `context` value directly, allowing you to organize and abstract hooks into reusable setup/teardown blocks. Please read [Context](#context-1) for examples and more information. 150 | 151 | ***Example: Lifecycle*** 152 | 153 | The following implements all available hooks so that their call patterns can be recorded: 154 | 155 | ```js 156 | test.before(() => { 157 | console.log('SETUP'); 158 | }); 159 | 160 | test.after(() => { 161 | console.log('CLEANUP'); 162 | }); 163 | 164 | test.before.each(() => { 165 | console.log('>> BEFORE'); 166 | }); 167 | 168 | test.after.each(() => { 169 | console.log('>> AFTER'); 170 | }); 171 | 172 | // --- 173 | 174 | test('foo', () => { 175 | console.log('>>>> TEST: FOO'); 176 | }); 177 | 178 | test('bar', () => { 179 | console.log('>>>> TEST: BAR'); 180 | }); 181 | 182 | test.run(); 183 | 184 | // SETUP 185 | // >> BEFORE 186 | // >>>> TEST: FOO 187 | // >> AFTER 188 | // >> BEFORE 189 | // >>>> TEST: BAR 190 | // >> AFTER 191 | // CLEANUP 192 | ``` 193 | 194 | 195 | ## Context 196 | 197 | When using [suite hooks](#hooks) to establish and reset environments, there's often some side-effect that you wish to make accessible to your tests. For example, this may be a HTTP client, a database table record, a JSDOM instance, etc. 198 | 199 | Typically, these side-effects would have to be saved into top-level variables that so that all parties involved can access them. The following is an example of this pattern: 200 | 201 | ```js 202 | const User = suite('User'); 203 | 204 | let client, user; 205 | 206 | User.before(async () => { 207 | client = await DB.connect(); 208 | }); 209 | 210 | User.before.each(async () => { 211 | user = await client.insert('insert into users ... returning *'); 212 | }); 213 | 214 | User.after.each(async () => { 215 | await client.destroy(`delete from users where id = ${user.id}`); 216 | user = undefined; 217 | }); 218 | 219 | User.after(async () => { 220 | client = await client.end(); 221 | }); 222 | 223 | User('should not have Teams initially', async () => { 224 | const teams = await client.select(` 225 | select id from users_teams 226 | where user_id = ${user.id}; 227 | `); 228 | 229 | assert.is(teams.length, 0); 230 | }); 231 | 232 | // ... 233 | 234 | User.run(); 235 | ``` 236 | 237 | While this certainly works, it can quickly become unruly once multiple suites exist within the same file. Additionally, it **requires** that all our suite hooks (`User.before`, `User.before.each`, etc) are defined within this file so that they may have access to the `user` and `client` variables that they're modifying. 238 | 239 | Instead, we can improve this by writing into the suite's "context" directly! 240 | 241 | > **Note:** If it helps your mental model, "context" can be interchanged with "state" – except that it's intended to umbrella the tests with a certain environment. 242 | 243 | ```js 244 | const User = suite('User'); 245 | 246 | User.before(async context => { 247 | context.client = await DB.connect(); 248 | }); 249 | 250 | User.before.each(async context => { 251 | context.user = await context.client.insert('insert into users ... returning *'); 252 | }); 253 | 254 | User.after.each(async context => { 255 | await context.client.destroy(`delete from users where id = ${user.id}`); 256 | context.user = undefined; 257 | }); 258 | 259 | User.after(async context => { 260 | context.client = await context.client.end(); 261 | }); 262 | 263 | // 264 | 265 | User.run(); 266 | ``` 267 | 268 | A "context" is unique to each suite and can be defined through [`suite()`](#uvusuitename-string) initialization and/or modified by the suite's hooks. Because of this, hooks can be abstracted into separate files and then attached safely to different suites: 269 | 270 | ```js 271 | import * as $ from './helpers'; 272 | 273 | const User = suite('User'); 274 | 275 | // Reuse generic/shared helpers 276 | // --- 277 | 278 | User.before($.DB.connect); 279 | User.after($.DB.destroy); 280 | 281 | // Keep User-specific helpers in this file 282 | // --- 283 | 284 | User.before.each(async context => { 285 | context.user = await context.client.insert('insert into users ... returning *'); 286 | }); 287 | 288 | User.after.each(async context => { 289 | await context.client.destroy(`delete from users where id = ${user.id}`); 290 | context.user = undefined; 291 | }); 292 | 293 | // 294 | 295 | User.run() 296 | ``` 297 | 298 | Individual tests will also receive the `context` value. This is how tests can access the HTTP client or database fixture you've set up, for example. 299 | 300 | Here's an example `User` test, now acessing its `user` and `client` values from context instead of globally-scoped variables: 301 | 302 | ```js 303 | User('should not have Teams initially', async context => { 304 | const { client, user } = context; 305 | 306 | const teams = await client.select(` 307 | select id from users_teams 308 | where user_id = ${user.id}; 309 | `); 310 | 311 | assert.is(teams.length, 0); 312 | }); 313 | ``` 314 | 315 | ***TypeScript*** 316 | 317 | Finally, TypeScript users can easily define their suites' contexts on a suite-by-suite basis. 318 | 319 | Let's revisit our initial example, now using context and TypeScript interfaces: 320 | 321 | ```ts 322 | interface Context { 323 | client?: DB.Client; 324 | user?: IUser; 325 | } 326 | 327 | const User = suite('User', { 328 | client: undefined, 329 | user: undefined, 330 | }); 331 | 332 | // Our `context` is type-checked 333 | // --- 334 | 335 | User.before(async context => { 336 | context.client = await DB.connect(); 337 | }); 338 | 339 | User.after(async context => { 340 | context.client = await context.client.end(); 341 | }); 342 | 343 | User.before.each(async context => { 344 | context.user = await context.client.insert('insert into users ... returning *'); 345 | }); 346 | 347 | User.after.each(async context => { 348 | await context.client.destroy(`delete from users where id = ${user.id}`); 349 | context.user = undefined; 350 | }); 351 | 352 | // Our `context` is *still* type-checked 🎉 353 | User('should not have Teams initially', async context => { 354 | const { client, user } = context; 355 | 356 | const teams = await client.select(` 357 | select id from users_teams 358 | where user_id = ${user.id}; 359 | `); 360 | 361 | assert.is(teams.length, 0); 362 | }); 363 | 364 | User.run(); 365 | ``` 366 | -------------------------------------------------------------------------------- /docs/cli.md: -------------------------------------------------------------------------------- 1 | # CLI 2 | 3 | > The `uvu` CLI is available whenever you install the `uvu` package. 4 | 5 | The _role_ of the `uvu` CLI is to collect and execute your tests suites. In order to do that, you must tell it how/where to find your tests. Otherwise, it has a default set of file patterns to search for – but that probably won't align with your project's structure. 6 | 7 | > **Note:** Using the `uvu` CLI is actually _optional_! See [Isolation](#isolation) for more info. 8 | 9 | Let's take a look at the CLI's help text: 10 | 11 | ```sh 12 | $ uvu --help 13 | # 14 | # Usage 15 | # $ uvu [dir] [pattern] [options] 16 | # 17 | # Options 18 | # -b, --bail Exit on first failure 19 | # -i, --ignore Any file patterns to ignore 20 | # -r, --require Additional module(s) to preload 21 | # -C, --cwd The current directory to resolve from (default .) 22 | # -c, --color Print colorized output (default true) 23 | # -v, --version Displays current version 24 | # -h, --help Displays this message 25 | # 26 | ``` 27 | 28 | As you can see, there are few arguments and option flags!
They're categorized by responsibility: 29 | 30 | * where/how to locate test files (`dir`, `pattern`, `--ignore`, and `--cwd`) 31 | * environment preparation (`--require` and `--color`) 32 | * whether or not to exit on suite failures (`--bail`) 33 | 34 | ## Matching 35 | 36 | ***Aside: Glob Patterns*** 37 | 38 | Unlike other test runners, `uvu` intentionally _does not_ rely on glob patterns.
Parsing and matching globs both require a non-trivial amount of work. (Even the smallest glob-parsers are as large – or larger – than the entire `uvu` source!) This price has to be paid upfront – at startup – and then becomes completely irrelevant. And then on the consumer side, glob patterns fall into one of two categories: 39 | 40 | * extremely simple patterns (eg, `test/**`, `tests/**/*.test.(ts|js)`, etc) 41 | * extremely complicated patterns that require squinting and fine-tuning 42 | 43 | Simple patterns don't _require_ a glob pattern 99% of the time.
44 | Complicated glob patterns can (often) be better expressed or understood as separate segments. 45 | 46 | And then there's the reoccurring issue of some shells and/or operating systems (eg, Windows) that will pre-evaluate a glob pattern, ruining a CLI's expected input... And how different systems require quotation marks, and/or certain characters to be escaped... There are issues with this approach. It certainly _can_ work, but `uvu` sidesteps this can of worms in favor of a different, but simpler approach. 47 | 48 | ***Logic*** 49 | 50 | The CLI will _always_ resolve lookups from the `--cwd` location, which is the `process.cwd()` when unspecified. 51 | 52 | By default (aka, when no arguments are given), `uvu` looks for files matching this behemoth of a pattern: 53 | 54 | ```js 55 | /((\/|^)(tests?|__tests?__)\/.*|\.(tests?|spec)|^\/?tests?)\.([mc]js|[jt]sx?)$/i; 56 | ``` 57 | 58 | This will: 59 | 60 | * search within `test`, `tests` or `__test__` or `__tests__` directories if they exist; otherwise any directory 61 | * search for files matching `*.test.{ext}` or `*.tests.{ext}` or `*.spec.{ext}` 62 | * search for files with these extensions: `mjs`, `cjs`, `js`, `jsx`, `tsx`, or `ts` 63 | 64 | Of course, you can help `uvu` out and narrow down its search party by providing a `dir` argument. Instead of searching from your project's root, `dir` tells `uvu` where to _start_ its traversal from. And by specifying a `dir`, the default `pattern` changes such that any file with a `mjs`, `cjs`, `js`, `jsx`, `tsx`, or `ts` extension will match. 65 | 66 | Finally, you may specify your own `pattern` too — assuming `dir` has been set.
The `pattern` value is passed through `new RegExp(, 'i')`. If you are nostalgic for complex glob patterns, this is your chance to relive its glory days – with substitutions, of course. 67 | 68 | For example, if we assume a [monorepo environment](/examples/monorepo/package.json), your `uvu` usage may look like this: 69 | 70 | ```sh 71 | $ uvu packages tests -i fixtures 72 | ``` 73 | 74 | This will traverse the `packages` directory, looking at files and subdirectories that match `/tests/i`, but ignore anything that matches the `/fixtures/i` pattern. 75 | 76 | 77 | ***Ignoring*** 78 | 79 | Any file or directory names matching `node_modules` or `^.git` are _always_ ignored. 80 | 81 | You can use the `-i/--ignore` flags to provide additional patterns to ignore. Much like `[pattern]`, these values are cast to a `RegExp`, allowing you to be as vague or specific as you need to be. 82 | 83 | The `-i/--ignore` flags can be passed multiple times: 84 | 85 | ```sh 86 | # Traverse "packages" diectory 87 | # ~> ignore items matching /foobar/i 88 | # ~> ignore items matching /fixtures/i 89 | # ~> ignore items matching /\d+.js$/i 90 | $ uvu packages -i foobar -i fixtures -i \\d+.js$ 91 | ``` 92 | 93 | 94 | ## Isolation 95 | 96 | When running `uvu`, it looks for all test files and will enqueue all suites for execution. You can always disable individual suites (by commenting out its `.run()` call), but sometimes you just want to execute a single file. 97 | 98 | To do this, you can simply call `node` with the path to your file! 🎉
This works _because_ `uvu` test files are self-contained – its `import`/`require` statements aren't tucked away, nor are the suites' `run()` invocation. 99 | 100 | ```sh 101 | # Before (runs everything in packages/**/**test**) 102 | $ uvu packages test 103 | 104 | # After (specific file) 105 | $ node packages/utils/test/random.js 106 | ``` 107 | 108 | Since `uvu` and `node` share the `--require` hook, you can bring your `uvu -r` arguments to `node` too~! 109 | 110 | ```sh 111 | # Before 112 | $ uvu -r esm tests 113 | $ uvu -r ts-node/register tests 114 | 115 | # After 116 | $ node -r esm tests/math.js 117 | $ node -r ts-node/register tests/math.ts 118 | ``` 119 | -------------------------------------------------------------------------------- /docs/coverage.md: -------------------------------------------------------------------------------- 1 | # Coverage 2 | 3 | Code coverage is not implemented by the [CLI](/docs/cli.md) directly. 4 | 5 | Instead, `uvu` plays nicely with existing coverage tools like [`c8`](https://www.npmjs.com/package/c8) or [`nyc`](https://www.npmjs.com/package/nyc).
Please refer to their respective documentations for usage information. 6 | 7 | ## Examples 8 | 9 | > Visit the working [`/examples/coverage`](/examples/coverage) demonstration~! 10 | 11 | Assuming we have a `uvu` command hooked up to the `npm test` script: 12 | 13 | ```js 14 | // package.json 15 | { 16 | "scripts": { 17 | "test": "uvu tests --ignore fixtures" 18 | } 19 | } 20 | ``` 21 | 22 | We can then use `nyc` or `c8` (or others) as a prefix to our `npm test` usage: 23 | 24 | ```sh 25 | $ c8 npm test 26 | $ nyc npm test 27 | 28 | $ c8 yarn test 29 | $ nyc yarn test 30 | 31 | $ nyc --include=src npm test 32 | $ c8 --all npm test 33 | ``` 34 | 35 | Of course, you can also use `c8`/`nyc` directly with `uvu` – it just makes it more confusing to distinguish CLI option flags: 36 | 37 | ```sh 38 | $ c8 uvu tests --ignore fixtures 39 | $ nyc --include=src uvu tests --ignore fixtures 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/esm.md: -------------------------------------------------------------------------------- 1 | # ES Modules 2 | 3 | EcmaScript Modules have landed in Node.js (> 12.x)! Check out the official language documentation[[1](https://nodejs.org/api/esm.html#esm_modules_ecmascript_modules)][[2](https://nodejs.org/api/packages.html)] to learn more. 4 | 5 | ...but, here's the **TL;DR:** 6 | 7 | * by default, only files with `.mjs` extension are treated as ESM 8 | * by default, `.js` – and now `.cjs` – files are treated as CommonJS 9 | * by defining `"type": "module"` in your `package.json` file, all `.js` files are treated as ESM 10 | * when using ESM, any `import`s must reference the _full_ filepath, including its extension 11 | * the `.cjs` extension is _always_ CommonJS, even if `"type": "module"` is defined 12 | 13 | ## Examples 14 | 15 | Knowing the above, there are a few ways we can use/integrate ESM into our `uvu` test suites! 16 | 17 | > **Important:** Only uvu v0.5.0+ has native ESM support 18 | 19 | ### Native ESM – via `.mjs` files 20 | 21 | > Visit the working [`/examples/esm.mjs`](/examples/esm.mjs) demonstration~! 22 | 23 | This example only works in Node.js v12.0 and later. In other words, it requires that _both_ your test files _and_ your source files possess the `.mjs` extension. This is – by default – the only way Node.js will load ES Modules and allow them to import/reference one another. 24 | 25 | ***PRO*** 26 | 27 | * Modern 28 | * Native / less tooling 29 | 30 | ***CON*** 31 | 32 | * Requires Node.js 12.0 and later 33 | * Exposes you to CommonJS <-> ESM interop issues 34 | * Cannot test older Node.js versions – unless maintain a duplicate set of source _and_ test files 35 | 36 | 37 | ### Polyfill – via `esm` package 38 | 39 | > Visit the working [`/examples/esm.loader`](/examples/esm.loader) demonstration~! 40 | 41 | Thanks to [`esm`](http://npmjs.com/package/esm), this example works in **all** Node.js versions. However, for best/consistent results, you **should avoid** using `.mjs` files when using this approach. This is because `esm` has some [limitations](https://www.npmjs.com/package/esm#extensions) and chooses not to interact/tamper with files that, by definition, should only be running with the native loader anyway. 42 | 43 | ***PRO*** 44 | 45 | * Makes ESM accessible to older Node.js versions 46 | * Solves (most) CommonJS <-> ESM interop issues 47 | * Only requires a simple `--require/-r` hook 48 | * Quick to attach and quick to execute 49 | 50 | ***CON*** 51 | 52 | * Not native 53 | * Not compatible with `.mjs` files 54 | 55 | 56 | ### Native ESM – via `"type": "module"` 57 | 58 | > Visit the working [`/examples/esm.dual`](/examples/esm.dual) demonstration~! 59 | 60 | This example combines the best of both worlds! It makes use of native ESM in Node.js versions that support it, while still making it possible to run your tests in older/legacy Node.js versions. 61 | 62 | With `"type": "module"`, we are able to use ESM within `.js` files. 63 | Node 12.x and later to process those files as ESM, through native behavior. 64 | And then older Node.js versions can run/process the _same_ files by simply including the [`esm`](http://npmjs.com/package/esm) loader. 65 | 66 | At worst – all we have is a "duplicate" test script... which is much, much better than duplicating sets of files. We end up with something like this: 67 | 68 | ```js 69 | { 70 | "type": "module", 71 | // ... 72 | "scripts": { 73 | "test:legacy": "uvu -r esm tests", 74 | "test:native": "uvu tests" 75 | } 76 | } 77 | ``` 78 | 79 | Your CI environment would execute the appropriate script according to its Node version :tada: 80 | 81 | ***PRO*** 82 | 83 | * Native when possible 84 | * No additional maintenance 85 | * Run tests in wider Node.js matrix 86 | * Easy to drop legacy support at anytime 87 | 88 | ***CON*** 89 | 90 | * Defining `"type": "module"` may change how your package is consumed 91 | -------------------------------------------------------------------------------- /docs/watch.md: -------------------------------------------------------------------------------- 1 | # Watch Mode 2 | 3 | Watching – aka "re-running" – tests is not implemented by the [CLI](/docs/cli.md) directly.
This partly because `uvu` is so fast and lightweight, which means that there's _very_ little cost in rerunning your tests. 4 | 5 | Instead, `uvu` is meant to interface with other tools nicely. Some of those include (but are not limited to): 6 | 7 | * [`watchlist`](https://github.com/lukeed/watchlist) 8 | * [`watchexec`](https://github.com/watchexec/watchexec) 9 | * [`chokidar-cli`](https://www.npmjs.com/package/chokidar-cli) 10 | * [`watch`](https://www.npmjs.com/package/watch) 11 | 12 | ## Example 13 | 14 | > Visit the working [`/examples/watch`](/examples/watch) demonstration~! 15 | 16 | Assuming we have a `uvu` command hooked up to the `npm test` script: 17 | 18 | ```js 19 | // package.json 20 | { 21 | "scripts": { 22 | "test": "uvu tests --ignore fixtures" 23 | }, 24 | "devDependencies": { 25 | "uvu": "^0.2.0" 26 | } 27 | } 28 | ``` 29 | 30 | We just need to install [`watchlist`](https://github.com/lukeed/watchlist) and configure a new `scripts` entry. We'll call it `"test:watch"`: 31 | 32 | > **Note:** As mentioned, alternatives to `watchlist` will work too. 33 | 34 | ```sh 35 | $ yarn add --dev watchlist 36 | ``` 37 | 38 | ```diff 39 | { 40 | "scripts": { 41 | - "test": "uvu tests --ignore fixtures" 42 | + "test": "uvu tests --ignore fixtures", 43 | + "test:watch": "watchlist src tests -- yarn test" 44 | }, 45 | "devDependencies": { 46 | - "uvu": "^0.2.0" 47 | + "uvu": "^0.2.0", 48 | + "watchlist": "^0.2.0" 49 | } 50 | } 51 | ``` 52 | 53 | Now we need to start our test watcher: 54 | 55 | ```sh 56 | $ yarn test:watch 57 | ``` 58 | 59 | What happens is that `watchlist` will recursively watch the `src` and `tests` directories. When those directories' contents change (move, rename, etc), the `yarn test` command will be executed. And in this case, `yarn test` is just an alias for our `uvu` configuration, which means that we can keep the command option flags visually separate & keep scripts DRY for our own peace of mind :) 60 | 61 | > **Note:** This assumes that we have a `/src` directory that lives alongside our `/tests` directory. 62 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "test": "uvu tests" 5 | }, 6 | "devDependencies": { 7 | "uvu": "^0.0.18" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/basic/src/math.js: -------------------------------------------------------------------------------- 1 | exports.sum = (a, b) => a + b; 2 | exports.div = (a, b) => a / b; 3 | exports.mod = (a, b) => a % b; 4 | -------------------------------------------------------------------------------- /examples/basic/src/utils.js: -------------------------------------------------------------------------------- 1 | exports.capitalize = function (str) { 2 | return str[0].toUpperCase() + str.substring(1); 3 | } 4 | 5 | exports.dashify = function (str) { 6 | return str.replace(/([a-zA-Z])(?=[A-Z\d])/g, '$1-').toLowerCase(); 7 | } 8 | -------------------------------------------------------------------------------- /examples/basic/tests/math.js: -------------------------------------------------------------------------------- 1 | const { test } = require('uvu'); 2 | const assert = require('uvu/assert'); 3 | const math = require('../src/math'); 4 | 5 | test('sum', () => { 6 | assert.type(math.sum, 'function'); 7 | assert.is(math.sum(1, 2), 3); 8 | assert.is(math.sum(-1, -2), -3); 9 | assert.is(math.sum(-1, 1), 0); 10 | }); 11 | 12 | test('div', () => { 13 | assert.type(math.div, 'function'); 14 | assert.is(math.div(1, 2), 0.5); 15 | assert.is(math.div(-1, -2), 0.5); 16 | assert.is(math.div(-1, 1), -1); 17 | }); 18 | 19 | test('mod', () => { 20 | assert.type(math.mod, 'function'); 21 | assert.is(math.mod(1, 2), 1); 22 | assert.is(math.mod(-3, -2), -1); 23 | assert.is(math.mod(7, 4), 3); 24 | }); 25 | 26 | test.run(); 27 | -------------------------------------------------------------------------------- /examples/basic/tests/utils.js: -------------------------------------------------------------------------------- 1 | const { test } = require('uvu'); 2 | const assert = require('uvu/assert'); 3 | const utils = require('../src/utils'); 4 | 5 | test('capitalize', () => { 6 | assert.type(utils.capitalize, 'function'); 7 | assert.is(utils.capitalize('hello'), 'Hello'); 8 | assert.is(utils.capitalize('foo bar'), 'Foo bar'); 9 | }); 10 | 11 | test('dashify', () => { 12 | assert.type(utils.dashify, 'function'); 13 | assert.is(utils.dashify('fooBar'), 'foo-bar'); 14 | assert.is(utils.dashify('FooBar'), 'foo-bar'); 15 | assert.is(utils.dashify('foobar'), 'foobar'); 16 | }); 17 | 18 | test.run(); 19 | -------------------------------------------------------------------------------- /examples/coverage/.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /coverage.* 3 | -------------------------------------------------------------------------------- /examples/coverage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "test": "uvu tests", 5 | "test:coverage": "c8 --include=src npm test", 6 | "test:report": "c8 report --reporter=text-lcov > coverage.lcov" 7 | }, 8 | "devDependencies": { 9 | "c8": "^7.2.0", 10 | "uvu": "^0.2.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/coverage/src/math.js: -------------------------------------------------------------------------------- 1 | exports.sum = (a, b) => a + b; 2 | exports.div = (a, b) => a / b; 3 | exports.mod = (a, b) => a % b; 4 | -------------------------------------------------------------------------------- /examples/coverage/src/utils.js: -------------------------------------------------------------------------------- 1 | exports.capitalize = function (str) { 2 | return str[0].toUpperCase() + str.substring(1); 3 | } 4 | 5 | exports.dashify = function (str) { 6 | return str.replace(/([a-zA-Z])(?=[A-Z\d])/g, '$1-').toLowerCase(); 7 | } 8 | -------------------------------------------------------------------------------- /examples/coverage/tests/math.js: -------------------------------------------------------------------------------- 1 | const { test } = require('uvu'); 2 | const assert = require('uvu/assert'); 3 | const math = require('../src/math'); 4 | 5 | test('sum', () => { 6 | assert.type(math.sum, 'function'); 7 | assert.is(math.sum(1, 2), 3); 8 | assert.is(math.sum(-1, -2), -3); 9 | assert.is(math.sum(-1, 1), 0); 10 | }); 11 | 12 | test('div', () => { 13 | assert.type(math.div, 'function'); 14 | assert.is(math.div(1, 2), 0.5); 15 | assert.is(math.div(-1, -2), 0.5); 16 | assert.is(math.div(-1, 1), -1); 17 | }); 18 | 19 | test('mod', () => { 20 | assert.type(math.mod, 'function'); 21 | assert.is(math.mod(1, 2), 1); 22 | assert.is(math.mod(-3, -2), -1); 23 | assert.is(math.mod(7, 4), 3); 24 | }); 25 | 26 | test.run(); 27 | -------------------------------------------------------------------------------- /examples/coverage/tests/utils.js: -------------------------------------------------------------------------------- 1 | const { test } = require('uvu'); 2 | const assert = require('uvu/assert'); 3 | const utils = require('../src/utils'); 4 | 5 | test('capitalize', () => { 6 | assert.type(utils.capitalize, 'function'); 7 | assert.is(utils.capitalize('hello'), 'Hello'); 8 | assert.is(utils.capitalize('foo bar'), 'Foo bar'); 9 | }); 10 | 11 | test('dashify', () => { 12 | assert.type(utils.dashify, 'function'); 13 | assert.is(utils.dashify('fooBar'), 'foo-bar'); 14 | assert.is(utils.dashify('FooBar'), 'foo-bar'); 15 | assert.is(utils.dashify('foobar'), 'foobar'); 16 | }); 17 | 18 | test.run(); 19 | -------------------------------------------------------------------------------- /examples/esbuild/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "test": "uvu -r esbuild-register tests" 5 | }, 6 | "devDependencies": { 7 | "esbuild": "0.13.3", 8 | "esbuild-register": "3.0.0", 9 | "uvu": "^0.5.1" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/esbuild/src/math.ts: -------------------------------------------------------------------------------- 1 | export function sum(a: number, b: number): number { 2 | return a + b; 3 | } 4 | 5 | export function div(a: number, b: number): number { 6 | return a / b; 7 | } 8 | 9 | export function mod(a: number, b: number): number { 10 | return a % b; 11 | } 12 | -------------------------------------------------------------------------------- /examples/esbuild/src/utils.ts: -------------------------------------------------------------------------------- 1 | export function capitalize(str: string): string { 2 | return str[0].toUpperCase() + str.substring(1); 3 | } 4 | 5 | export function dashify(str: string): string { 6 | return str.replace(/([a-zA-Z])(?=[A-Z\d])/g, '$1-').toLowerCase(); 7 | } 8 | -------------------------------------------------------------------------------- /examples/esbuild/tests/math.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as math from '../src/math'; 4 | 5 | test('sum', () => { 6 | assert.type(math.sum, 'function'); 7 | assert.is(math.sum(1, 2), 3); 8 | assert.is(math.sum(-1, -2), -3); 9 | assert.is(math.sum(-1, 1), 0); 10 | }); 11 | 12 | test('div', () => { 13 | assert.type(math.div, 'function'); 14 | assert.is(math.div(1, 2), 0.5); 15 | assert.is(math.div(-1, -2), 0.5); 16 | assert.is(math.div(-1, 1), -1); 17 | }); 18 | 19 | test('mod', () => { 20 | assert.type(math.mod, 'function'); 21 | assert.is(math.mod(1, 2), 1); 22 | assert.is(math.mod(-3, -2), -1); 23 | assert.is(math.mod(7, 4), 3); 24 | }); 25 | 26 | test.run(); 27 | -------------------------------------------------------------------------------- /examples/esbuild/tests/utils.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as utils from '../src/utils'; 4 | 5 | test('capitalize', () => { 6 | assert.type(utils.capitalize, 'function'); 7 | assert.is(utils.capitalize('hello'), 'Hello'); 8 | assert.is(utils.capitalize('foo bar'), 'Foo bar'); 9 | }); 10 | 11 | test('dashify', () => { 12 | assert.type(utils.dashify, 'function'); 13 | assert.is(utils.dashify('fooBar'), 'foo-bar'); 14 | assert.is(utils.dashify('FooBar'), 'foo-bar'); 15 | assert.is(utils.dashify('foobar'), 'foobar'); 16 | }); 17 | 18 | test.run(); 19 | -------------------------------------------------------------------------------- /examples/esbuild/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build", 4 | "target": "esnext", 5 | "module": "esnext", 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "forceConsistentCasingInFileNames": true 9 | }, 10 | "include": [ 11 | "@types/**/*", 12 | "src/**/*" 13 | ], 14 | "exclude": [ 15 | "node_modules" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /examples/esm.dual/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "test:legacy": "uvu -r esm tests", 6 | "test:native": "uvu tests" 7 | }, 8 | "devDependencies": { 9 | "esm": "3.2.25", 10 | "uvu": "^0.5.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/esm.dual/readme.md: -------------------------------------------------------------------------------- 1 | # Example: esm.dual 2 | 3 | Please read [/docs/esm](/docs/esm.md) for full details & comparisons. 4 | 5 | ## Why 6 | 7 | Unlike [/examples/esm.loader](/examples/esm.loader), this example uses the native ESM loader whenever it's available. 8 | 9 | Unlike [/examples/esm.mjs](/examples/esm.mjs), this example will run in all versions of Node.js – including older versions where ESM is not natively supported. 10 | 11 | 12 | ## Highlights 13 | 14 | * Define `"type": "module"` within `package.json`
Allows Node.js to treat `.js` files as ESM. 15 | 16 | * Define `import` statements with full file paths
Required by Node.js whenever ESM in use. 17 | 18 | * Define two `test` scripts: 19 | * `"test:native"` – for use within Node 12+ 20 | * `"test:legacy"` – for use with Node < 12 21 | 22 | ## License 23 | 24 | MIT © [Luke Edwards](https://lukeed.com) 25 | -------------------------------------------------------------------------------- /examples/esm.dual/src/math.js: -------------------------------------------------------------------------------- 1 | export const sum = (a, b) => a + b; 2 | export const div = (a, b) => a / b; 3 | export const mod = (a, b) => a % b; 4 | -------------------------------------------------------------------------------- /examples/esm.dual/src/utils.js: -------------------------------------------------------------------------------- 1 | export function capitalize(str) { 2 | return str[0].toUpperCase() + str.substring(1); 3 | } 4 | 5 | export function dashify(str) { 6 | return str.replace(/([a-zA-Z])(?=[A-Z\d])/g, '$1-').toLowerCase(); 7 | } 8 | -------------------------------------------------------------------------------- /examples/esm.dual/tests/math.js: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as math from '../src/math.js'; 4 | 5 | const sum = suite('sum'); 6 | 7 | sum('should be a function', () => { 8 | assert.type(math.sum, 'function'); 9 | }); 10 | 11 | sum('should compute values', () => { 12 | assert.is(math.sum(1, 2), 3); 13 | assert.is(math.sum(-1, -2), -3); 14 | assert.is(math.sum(-1, 1), 0); 15 | }); 16 | 17 | sum.run(); 18 | 19 | // --- 20 | 21 | const div = suite('div'); 22 | 23 | div('should be a function', () => { 24 | assert.type(math.div, 'function'); 25 | }); 26 | 27 | div('should compute values', () => { 28 | assert.is(math.div(1, 2), 0.5); 29 | assert.is(math.div(-1, -2), 0.5); 30 | assert.is(math.div(-1, 1), -1); 31 | }); 32 | 33 | div.run(); 34 | 35 | // --- 36 | 37 | const mod = suite('mod'); 38 | 39 | mod('should be a function', () => { 40 | assert.type(math.mod, 'function'); 41 | }); 42 | 43 | mod('should compute values', () => { 44 | assert.is(math.mod(1, 2), 1); 45 | assert.is(math.mod(-3, -2), -1); 46 | assert.is(math.mod(7, 4), 3); 47 | }); 48 | 49 | mod.run(); 50 | -------------------------------------------------------------------------------- /examples/esm.dual/tests/utils.js: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as utils from '../src/utils.js'; 4 | 5 | const capitalize = suite('capitalize'); 6 | 7 | capitalize('should be a function', () => { 8 | assert.type(utils.capitalize, 'function'); 9 | }); 10 | 11 | capitalize('should capitalize a word', () => { 12 | assert.is(utils.capitalize('hello'), 'Hello'); 13 | }); 14 | 15 | capitalize('should only capitalize the 1st word', () => { 16 | assert.is(utils.capitalize('foo bar'), 'Foo bar'); 17 | }); 18 | 19 | capitalize.run(); 20 | 21 | // --- 22 | 23 | const dashify = suite('dashify'); 24 | 25 | dashify('should be a function', () => { 26 | assert.type(utils.dashify, 'function'); 27 | }); 28 | 29 | dashify('should replace camelCase with dash-case', () => { 30 | assert.is(utils.dashify('fooBar'), 'foo-bar'); 31 | assert.is(utils.dashify('FooBar'), 'foo-bar'); 32 | }); 33 | 34 | dashify('should enforce lowercase', () => { 35 | assert.is(utils.dashify('foobar'), 'foobar'); 36 | }); 37 | 38 | dashify.run(); 39 | -------------------------------------------------------------------------------- /examples/esm.loader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "test": "uvu -r esm tests" 5 | }, 6 | "devDependencies": { 7 | "esm": "3.2.25", 8 | "uvu": "^0.5.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/esm.loader/readme.md: -------------------------------------------------------------------------------- 1 | # Example: esm.loader 2 | 3 | Please read [/docs/esm](/docs/esm.md) for full details & comparisons. 4 | 5 | ## Why 6 | 7 | This example makes use of the [`esm`](https://npmjs.com/package/esm) module, which rewrites all ESM syntax to CommonJS on the fly. 8 | 9 | 10 | ## Highlights 11 | 12 | * Use ESM within regular `.js` files 13 | 14 | * Works in all versions of Node.js
Because the `esm` loader is invoked – never the native behavior. 15 | 16 | * Solves CommonJS <-> ESM interop issues
A significant portion of the npm ecosystem is still CommonJS-only. 17 | 18 | 19 | ## License 20 | 21 | MIT © [Luke Edwards](https://lukeed.com) 22 | -------------------------------------------------------------------------------- /examples/esm.loader/src/math.js: -------------------------------------------------------------------------------- 1 | export const sum = (a, b) => a + b; 2 | export const div = (a, b) => a / b; 3 | export const mod = (a, b) => a % b; 4 | -------------------------------------------------------------------------------- /examples/esm.loader/src/utils.js: -------------------------------------------------------------------------------- 1 | export function capitalize(str) { 2 | return str[0].toUpperCase() + str.substring(1); 3 | } 4 | 5 | export function dashify(str) { 6 | return str.replace(/([a-zA-Z])(?=[A-Z\d])/g, '$1-').toLowerCase(); 7 | } 8 | -------------------------------------------------------------------------------- /examples/esm.loader/tests/math.js: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as math from '../src/math'; 4 | 5 | const sum = suite('sum'); 6 | 7 | sum('should be a function', () => { 8 | assert.type(math.sum, 'function'); 9 | }); 10 | 11 | sum('should compute values', () => { 12 | assert.is(math.sum(1, 2), 3); 13 | assert.is(math.sum(-1, -2), -3); 14 | assert.is(math.sum(-1, 1), 0); 15 | }); 16 | 17 | sum.run(); 18 | 19 | // --- 20 | 21 | const div = suite('div'); 22 | 23 | div('should be a function', () => { 24 | assert.type(math.div, 'function'); 25 | }); 26 | 27 | div('should compute values', () => { 28 | assert.is(math.div(1, 2), 0.5); 29 | assert.is(math.div(-1, -2), 0.5); 30 | assert.is(math.div(-1, 1), -1); 31 | }); 32 | 33 | div.run(); 34 | 35 | // --- 36 | 37 | const mod = suite('mod'); 38 | 39 | mod('should be a function', () => { 40 | assert.type(math.mod, 'function'); 41 | }); 42 | 43 | mod('should compute values', () => { 44 | assert.is(math.mod(1, 2), 1); 45 | assert.is(math.mod(-3, -2), -1); 46 | assert.is(math.mod(7, 4), 3); 47 | }); 48 | 49 | mod.run(); 50 | -------------------------------------------------------------------------------- /examples/esm.loader/tests/utils.js: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as utils from '../src/utils'; 4 | 5 | const capitalize = suite('capitalize'); 6 | 7 | capitalize('should be a function', () => { 8 | assert.type(utils.capitalize, 'function'); 9 | }); 10 | 11 | capitalize('should capitalize a word', () => { 12 | assert.is(utils.capitalize('hello'), 'Hello'); 13 | }); 14 | 15 | capitalize('should only capitalize the 1st word', () => { 16 | assert.is(utils.capitalize('foo bar'), 'Foo bar'); 17 | }); 18 | 19 | capitalize.run(); 20 | 21 | // --- 22 | 23 | const dashify = suite('dashify'); 24 | 25 | dashify('should be a function', () => { 26 | assert.type(utils.dashify, 'function'); 27 | }); 28 | 29 | dashify('should replace camelCase with dash-case', () => { 30 | assert.is(utils.dashify('fooBar'), 'foo-bar'); 31 | assert.is(utils.dashify('FooBar'), 'foo-bar'); 32 | }); 33 | 34 | dashify('should enforce lowercase', () => { 35 | assert.is(utils.dashify('foobar'), 'foobar'); 36 | }); 37 | 38 | dashify.run(); 39 | -------------------------------------------------------------------------------- /examples/esm.mjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "test": "uvu tests" 5 | }, 6 | "devDependencies": { 7 | "uvu": "^0.5.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/esm.mjs/readme.md: -------------------------------------------------------------------------------- 1 | # Example: esm.mjs 2 | 3 | Please read [/docs/esm](/docs/esm.md) for full details & comparisons. 4 | 5 | ## Why 6 | 7 | This example makes use of the native ESM loader – available in version Node 12 and later! 8 | 9 | 10 | ## Highlights 11 | 12 | * Uses native ESM via the `.mjs` file extension
This is the Node's default behavior. 13 | 14 | * Define `import` statements with full file paths
Required by Node.js whenever ESM in use. 15 | 16 | 17 | ## License 18 | 19 | MIT © [Luke Edwards](https://lukeed.com) 20 | -------------------------------------------------------------------------------- /examples/esm.mjs/src/math.mjs: -------------------------------------------------------------------------------- 1 | export const sum = (a, b) => a + b; 2 | export const div = (a, b) => a / b; 3 | export const mod = (a, b) => a % b; 4 | -------------------------------------------------------------------------------- /examples/esm.mjs/src/utils.mjs: -------------------------------------------------------------------------------- 1 | export function capitalize(str) { 2 | return str[0].toUpperCase() + str.substring(1); 3 | } 4 | 5 | export function dashify(str) { 6 | return str.replace(/([a-zA-Z])(?=[A-Z\d])/g, '$1-').toLowerCase(); 7 | } 8 | -------------------------------------------------------------------------------- /examples/esm.mjs/tests/math.mjs: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as math from '../src/math.mjs'; 4 | 5 | const sum = suite('sum'); 6 | 7 | sum('should be a function', () => { 8 | assert.type(math.sum, 'function'); 9 | }); 10 | 11 | sum('should compute values', () => { 12 | assert.is(math.sum(1, 2), 3); 13 | assert.is(math.sum(-1, -2), -3); 14 | assert.is(math.sum(-1, 1), 0); 15 | }); 16 | 17 | sum.run(); 18 | 19 | // --- 20 | 21 | const div = suite('div'); 22 | 23 | div('should be a function', () => { 24 | assert.type(math.div, 'function'); 25 | }); 26 | 27 | div('should compute values', () => { 28 | assert.is(math.div(1, 2), 0.5); 29 | assert.is(math.div(-1, -2), 0.5); 30 | assert.is(math.div(-1, 1), -1); 31 | }); 32 | 33 | div.run(); 34 | 35 | // --- 36 | 37 | const mod = suite('mod'); 38 | 39 | mod('should be a function', () => { 40 | assert.type(math.mod, 'function'); 41 | }); 42 | 43 | mod('should compute values', () => { 44 | assert.is(math.mod(1, 2), 1); 45 | assert.is(math.mod(-3, -2), -1); 46 | assert.is(math.mod(7, 4), 3); 47 | }); 48 | 49 | mod.run(); 50 | -------------------------------------------------------------------------------- /examples/esm.mjs/tests/utils.mjs: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as utils from '../src/utils.mjs'; 4 | 5 | const capitalize = suite('capitalize'); 6 | 7 | capitalize('should be a function', () => { 8 | assert.type(utils.capitalize, 'function'); 9 | }); 10 | 11 | capitalize('should capitalize a word', () => { 12 | assert.is(utils.capitalize('hello'), 'Hello'); 13 | }); 14 | 15 | capitalize('should only capitalize the 1st word', () => { 16 | assert.is(utils.capitalize('foo bar'), 'Foo bar'); 17 | }); 18 | 19 | capitalize.run(); 20 | 21 | // --- 22 | 23 | const dashify = suite('dashify'); 24 | 25 | dashify('should be a function', () => { 26 | assert.type(utils.dashify, 'function'); 27 | }); 28 | 29 | dashify('should replace camelCase with dash-case', () => { 30 | assert.is(utils.dashify('fooBar'), 'foo-bar'); 31 | assert.is(utils.dashify('FooBar'), 'foo-bar'); 32 | }); 33 | 34 | dashify('should enforce lowercase', () => { 35 | assert.is(utils.dashify('foobar'), 'foobar'); 36 | }); 37 | 38 | dashify.run(); 39 | -------------------------------------------------------------------------------- /examples/monorepo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "test": "uvu packages tests" 5 | }, 6 | "devDependencies": { 7 | "uvu": "^0.0.18" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/monorepo/packages/math/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "version": "0.0.0", 4 | "name": "@demo/math" 5 | } 6 | -------------------------------------------------------------------------------- /examples/monorepo/packages/math/src/index.js: -------------------------------------------------------------------------------- 1 | exports.sum = (a, b) => a + b; 2 | exports.div = (a, b) => a / b; 3 | exports.mod = (a, b) => a % b; 4 | -------------------------------------------------------------------------------- /examples/monorepo/packages/math/tests/index.js: -------------------------------------------------------------------------------- 1 | const { suite } = require('uvu'); 2 | const assert = require('uvu/assert'); 3 | const math = require('../src'); 4 | 5 | const sum = suite('sum'); 6 | 7 | sum('should be a function', () => { 8 | assert.type(math.sum, 'function'); 9 | }); 10 | 11 | sum('should compute values', () => { 12 | assert.is(math.sum(1, 2), 3); 13 | assert.is(math.sum(-1, -2), -3); 14 | assert.is(math.sum(-1, 1), 0); 15 | }); 16 | 17 | sum.run(); 18 | 19 | // --- 20 | 21 | const div = suite('div'); 22 | 23 | div('should be a function', () => { 24 | assert.type(math.div, 'function'); 25 | }); 26 | 27 | div('should compute values', () => { 28 | assert.is(math.div(1, 2), 0.5); 29 | assert.is(math.div(-1, -2), 0.5); 30 | assert.is(math.div(-1, 1), -1); 31 | }); 32 | 33 | div.run(); 34 | 35 | // --- 36 | 37 | const mod = suite('mod'); 38 | 39 | mod('should be a function', () => { 40 | assert.type(math.mod, 'function'); 41 | }); 42 | 43 | mod('should compute values', () => { 44 | assert.is(math.mod(1, 2), 1); 45 | assert.is(math.mod(-3, -2), -1); 46 | assert.is(math.mod(7, 4), 3); 47 | }); 48 | 49 | mod.run(); 50 | -------------------------------------------------------------------------------- /examples/monorepo/packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "version": "0.0.0", 4 | "name": "@demo/utils" 5 | } 6 | -------------------------------------------------------------------------------- /examples/monorepo/packages/utils/src/index.js: -------------------------------------------------------------------------------- 1 | exports.capitalize = function (str) { 2 | return str[0].toUpperCase() + str.substring(1); 3 | } 4 | 5 | exports.dashify = function (str) { 6 | return str.replace(/([a-zA-Z])(?=[A-Z\d])/g, '$1-').toLowerCase(); 7 | } 8 | -------------------------------------------------------------------------------- /examples/monorepo/packages/utils/tests/index.js: -------------------------------------------------------------------------------- 1 | const { suite } = require('uvu'); 2 | const assert = require('uvu/assert'); 3 | const utils = require('../src'); 4 | 5 | const capitalize = suite('capitalize'); 6 | 7 | capitalize('should be a function', () => { 8 | assert.type(utils.capitalize, 'function'); 9 | }); 10 | 11 | capitalize('should capitalize a word', () => { 12 | assert.is(utils.capitalize('hello'), 'Hello'); 13 | }); 14 | 15 | capitalize('should only capitalize the 1st word', () => { 16 | assert.is(utils.capitalize('foo bar'), 'Foo bar'); 17 | }); 18 | 19 | capitalize.run(); 20 | 21 | // --- 22 | 23 | const dashify = suite('dashify'); 24 | 25 | dashify('should be a function', () => { 26 | assert.type(utils.dashify, 'function'); 27 | }); 28 | 29 | dashify('should replace camelCase with dash-case', () => { 30 | assert.is(utils.dashify('fooBar'), 'foo-bar'); 31 | assert.is(utils.dashify('FooBar'), 'foo-bar'); 32 | }); 33 | 34 | dashify('should enforce lowercase', () => { 35 | assert.is(utils.dashify('foobar'), 'foobar'); 36 | }); 37 | 38 | dashify.run(); 39 | -------------------------------------------------------------------------------- /examples/preact/.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@babel/plugin-syntax-jsx", 4 | ["@babel/plugin-transform-react-jsx", { 5 | "pragmaFrag": "Fragment", 6 | "pragma": "h" 7 | }] 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /examples/preact/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "test": "uvu tests -r esm -r @babel/register -i setup" 5 | }, 6 | "dependencies": { 7 | "preact": "^10.4.5" 8 | }, 9 | "devDependencies": { 10 | "@babel/core": "7.10.4", 11 | "@babel/register": "7.10.4", 12 | "@babel/plugin-syntax-jsx": "7.10.4", 13 | "@babel/plugin-transform-react-jsx": "7.10.4", 14 | "esm": "3.2.25", 15 | "jsdom": "16.3.0", 16 | "uvu": "^0.2.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/preact/src/Count.jsx: -------------------------------------------------------------------------------- 1 | import { h, Fragment } from 'preact'; 2 | import { useState } from 'preact/hooks'; 3 | 4 | export default function Counter(props) { 5 | const { count=5 } = props; 6 | const [value, setValue] = useState(count); 7 | 8 | return ( 9 | <> 10 | 11 | {value} 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /examples/preact/tests/Count.js: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as ENV from './setup/env'; 4 | 5 | // Relies on `@babel/register` 6 | import Count from '../src/Count.jsx'; 7 | 8 | test.before(ENV.setup); 9 | test.before.each(ENV.reset); 10 | 11 | test('should render with "5" by default', () => { 12 | const { container } = ENV.render(Count); 13 | 14 | assert.snapshot( 15 | container.innerHTML, 16 | `5` 17 | ); 18 | }); 19 | 20 | test('should accept custom `count` prop', () => { 21 | const { container } = ENV.render(Count, { count: 99 }); 22 | 23 | assert.snapshot( 24 | container.innerHTML, 25 | `99` 26 | ); 27 | }); 28 | 29 | test('should increment count after `button#incr` click', async () => { 30 | const { container } = ENV.render(Count); 31 | 32 | assert.snapshot( 33 | container.innerHTML, 34 | `5` 35 | ); 36 | 37 | await ENV.fire( 38 | container.querySelector('#incr'), 39 | 'click' 40 | ); 41 | 42 | assert.snapshot( 43 | container.innerHTML, 44 | `6` 45 | ); 46 | }); 47 | 48 | test('should decrement count after `button#decr` click', async () => { 49 | const { container } = ENV.render(Count); 50 | 51 | assert.snapshot( 52 | container.innerHTML, 53 | `5` 54 | ); 55 | 56 | await ENV.fire( 57 | container.querySelector('#decr'), 58 | 'click' 59 | ); 60 | 61 | assert.snapshot( 62 | container.innerHTML, 63 | `4` 64 | ); 65 | }); 66 | 67 | test.run(); 68 | -------------------------------------------------------------------------------- /examples/preact/tests/setup/env.js: -------------------------------------------------------------------------------- 1 | import { JSDOM } from 'jsdom'; 2 | import * as Preact from 'preact'; 3 | import { act } from 'preact/test-utils'; 4 | 5 | const { window } = new JSDOM('
'); 6 | 7 | export function setup() { 8 | // @ts-ignore 9 | global.window = window; 10 | global.document = window.document; 11 | global.navigator = window.navigator; 12 | global.getComputedStyle = window.getComputedStyle; 13 | global.requestAnimationFrame = null; 14 | } 15 | 16 | export function reset() { 17 | window.document.title = ''; 18 | window.document.head.innerHTML = ''; 19 | window.document.body.innerHTML = '
'; 20 | } 21 | 22 | /** 23 | * @typedef RenderOutput 24 | * @property container {HTMLElement} 25 | * @property component {Preact.VNode} 26 | */ 27 | 28 | /** 29 | * @return {RenderOutput} 30 | */ 31 | export function render(Tag, props = {}) { 32 | const container = window.document.querySelector('main'); 33 | const component = Preact.h(Tag, props); 34 | Preact.render(component, container) 35 | return { container, component }; 36 | } 37 | 38 | /** 39 | * @param {HTMLElement} elem 40 | * @param {String} event 41 | * @param {any} [details] 42 | */ 43 | export async function fire(elem, event, details) { 44 | await act(() => { 45 | let evt = new window.Event(event, details); 46 | elem.dispatchEvent(evt); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /examples/puppeteer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "start": "sirv src --dev", 6 | "test": "uvu tests -i setup" 7 | }, 8 | "devDependencies": { 9 | "puppeteer": "^5.5.0", 10 | "sirv-cli": "^1.0.6", 11 | "uvu": "^0.5.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/puppeteer/readme.md: -------------------------------------------------------------------------------- 1 | # Example: Puppeteer 2 | 3 | This example runs [Puppeteer](https://www.npmjs.com/package/puppeteer) programmatically. 4 | 5 | In order to invoke and test against source (`/src`) files, this example also uses [`sirv-cli`](https://www.npmjs.com/package/sirv-cli) to run a local file server. Puppeteer connects to this file server to access the `window` globals that `src/math.js` and `src/utils.js` are globally mounted to. 6 | 7 | > **Note:** Window globals are not required in order for `uvu` to work with `puppeteer`!
You may, for example, load your Preact, Svelte, Vue, ... etc application inside the devserver & interact with it through Puppeteer APIs! 8 | 9 | ## Setup 10 | 11 | ```sh 12 | $ npm install 13 | ``` 14 | 15 | ## Testing 16 | 17 | ```sh 18 | # start server 19 | $ npm start 20 | 21 | # run tests (2nd terminal) 22 | $ npm test 23 | ``` 24 | 25 | ## License 26 | 27 | MIT 28 | -------------------------------------------------------------------------------- /examples/puppeteer/src/app.js: -------------------------------------------------------------------------------- 1 | import * as math from './math.js'; 2 | import * as utils from './utils.js'; 3 | 4 | window.__MATH__ = math; 5 | window.__UTILS__ = utils; 6 | -------------------------------------------------------------------------------- /examples/puppeteer/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | uvu + Puppeteer 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/puppeteer/src/math.js: -------------------------------------------------------------------------------- 1 | export const sum = (a, b) => a + b; 2 | export const div = (a, b) => a / b; 3 | export const mod = (a, b) => a % b; 4 | -------------------------------------------------------------------------------- /examples/puppeteer/src/utils.js: -------------------------------------------------------------------------------- 1 | export function capitalize(str) { 2 | return str[0].toUpperCase() + str.substring(1); 3 | } 4 | 5 | export function dashify(str) { 6 | return str.replace(/([a-zA-Z])(?=[A-Z\d])/g, '$1-').toLowerCase(); 7 | } 8 | -------------------------------------------------------------------------------- /examples/puppeteer/tests/index.js: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as ENV from './setup/puppeteer.js' 4 | 5 | test.before(ENV.setup); 6 | test.after(ENV.reset); 7 | 8 | test('can fetch data!', async context => { 9 | const data = await context.page.evaluate(() => { 10 | return fetch('https://httpbin.org/get').then(r => r.json()); 11 | }); 12 | 13 | assert.type(data, 'object'); 14 | assert.is(data.url, 'https://httpbin.org/get'); 15 | }); 16 | 17 | test('can select elements!', async context => { 18 | await context.page.goto('http://example.com/'); 19 | 20 | const text = await context.page.evaluate(() => { 21 | return document.querySelector('h1').textContent; 22 | }); 23 | 24 | assert.type(text, 'string'); 25 | assert.is(text, 'Example Domain'); 26 | }); 27 | 28 | test.run(); 29 | -------------------------------------------------------------------------------- /examples/puppeteer/tests/math.js: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as ENV from './setup/puppeteer.js' 4 | 5 | const sum = suite('sum'); 6 | sum.before(ENV.setup); 7 | sum.before.each(ENV.homepage); 8 | sum.after(ENV.reset); 9 | 10 | sum('should be a function', async context => { 11 | assert.is( 12 | await context.page.evaluate(() => typeof window.__MATH__.sum), 13 | 'function' 14 | ); 15 | }); 16 | 17 | sum('should compute values', async context => { 18 | const { page } = context; 19 | 20 | assert.is(await page.evaluate(() => window.__MATH__.sum(1, 2)), 3); 21 | assert.is(await page.evaluate(() => window.__MATH__.sum(-1, -2)), -3); 22 | assert.is(await page.evaluate(() => window.__MATH__.sum(-1, 1)), 0); 23 | }); 24 | 25 | sum.run(); 26 | 27 | // --- 28 | 29 | const div = suite('div'); 30 | div.before(ENV.setup); 31 | div.before.each(ENV.homepage); 32 | div.after(ENV.reset); 33 | 34 | div('should be a function', async context => { 35 | assert.is( 36 | await context.page.evaluate(() => typeof window.__MATH__.div), 37 | 'function' 38 | ); 39 | }); 40 | 41 | div('should compute values', async context => { 42 | const { page } = context; 43 | 44 | assert.is(await page.evaluate(() => window.__MATH__.div(1, 2)), 0.5); 45 | assert.is(await page.evaluate(() => window.__MATH__.div(-1, -2)), 0.5); 46 | assert.is(await page.evaluate(() => window.__MATH__.div(-1, 1)), -1); 47 | }); 48 | 49 | div.run(); 50 | 51 | // --- 52 | 53 | const mod = suite('mod'); 54 | mod.before(ENV.setup); 55 | mod.before.each(ENV.homepage); 56 | mod.after(ENV.reset); 57 | 58 | mod('should be a function', async context => { 59 | assert.is( 60 | await context.page.evaluate(() => typeof window.__MATH__.mod), 61 | 'function' 62 | ); 63 | }); 64 | 65 | mod('should compute values', async context => { 66 | const { page } = context; 67 | 68 | assert.is(await page.evaluate(() => window.__MATH__.mod(1, 2)), 1); 69 | assert.is(await page.evaluate(() => window.__MATH__.mod(-1, -2)), -1); 70 | assert.is(await page.evaluate(() => window.__MATH__.mod(7, 4)), 3); 71 | }); 72 | 73 | mod.run(); 74 | -------------------------------------------------------------------------------- /examples/puppeteer/tests/setup/puppeteer.js: -------------------------------------------------------------------------------- 1 | import Chrome from 'puppeteer'; 2 | 3 | // Launch the browser 4 | // Add `browser` and `page` to context 5 | export async function setup(context) { 6 | context.browser = await Chrome.launch(); 7 | context.page = await context.browser.newPage(); 8 | } 9 | 10 | // Close everything on suite completion 11 | export async function reset(context) { 12 | await context.page.close(); 13 | await context.browser.close(); 14 | } 15 | 16 | // Navigate to homepage 17 | export async function homepage(context) { 18 | await context.page.goto('http://localhost:5000'); 19 | } 20 | -------------------------------------------------------------------------------- /examples/puppeteer/tests/utils.js: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as ENV from './setup/puppeteer.js' 4 | 5 | const capitalize = suite('capitalize'); 6 | capitalize.before(ENV.setup); 7 | capitalize.before.each(ENV.homepage); 8 | capitalize.after(ENV.reset); 9 | 10 | capitalize('should be a function', async context => { 11 | assert.is( 12 | await context.page.evaluate(() => typeof window.__UTILS__.capitalize), 13 | 'function' 14 | ); 15 | }); 16 | 17 | capitalize('should capitalize a word', async context => { 18 | assert.is( 19 | await context.page.evaluate(() => window.__UTILS__.capitalize('hello')), 20 | 'Hello' 21 | ); 22 | }); 23 | 24 | capitalize('should only capitalize the 1st word', async context => { 25 | assert.is( 26 | await context.page.evaluate(() => window.__UTILS__.capitalize('foo bar')), 27 | 'Foo bar' 28 | ); 29 | }); 30 | 31 | capitalize.run(); 32 | 33 | // --- 34 | 35 | const dashify = suite('dashify'); 36 | dashify.before(ENV.setup); 37 | dashify.before.each(ENV.homepage); 38 | dashify.after(ENV.reset); 39 | 40 | dashify('should be a function', async context => { 41 | assert.is( 42 | await context.page.evaluate(() => typeof window.__UTILS__.dashify), 43 | 'function' 44 | ); 45 | }); 46 | 47 | dashify('should replace camelCase with dash-case', async context => { 48 | const { page } = context; 49 | assert.is(await page.evaluate(() => window.__UTILS__.dashify('fooBar')), 'foo-bar'); 50 | assert.is(await page.evaluate(() => window.__UTILS__.dashify('FooBar')), 'foo-bar'); 51 | }); 52 | 53 | dashify('should ignore lowercase', async context => { 54 | const { page } = context; 55 | assert.is(await page.evaluate(() => window.__UTILS__.dashify('foobar')), 'foobar'); 56 | }); 57 | 58 | dashify.run(); 59 | -------------------------------------------------------------------------------- /examples/solidjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "test": "uvu tests -r solid-register" 5 | }, 6 | "dependencies": { 7 | "solid-js": "^1.2.0" 8 | }, 9 | "devDependencies": { 10 | "solid-register": "0.0.5", 11 | "solid-testing-library": "0.2.1", 12 | "jsdom": "18.0.0", 13 | "uvu": "0.5.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/solidjs/src/Count.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal, Component } from 'solid-js' 2 | 3 | export type CountProps = { count?: number }; 4 | 5 | export const Count: Component = (props) => { 6 | const [value, setValue] = createSignal(props.count ?? 5); 7 | 8 | return <> 9 | 10 | {value()} 11 | 12 | 13 | } 14 | -------------------------------------------------------------------------------- /examples/solidjs/tests/Count.test.tsx: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | 4 | import { cleanup, render, fireEvent, screen } from 'solid-testing-library'; 5 | 6 | import { Count } from '../src/Count'; 7 | 8 | const isInDom = (node: Node): boolean => !!node.parentNode && 9 | (node.parentNode === document || isInDom(node.parentNode)); 10 | 11 | test.after.each(cleanup); 12 | 13 | test('should render with "5" by default', () => { 14 | const { container } = render(() => ); 15 | 16 | assert.snapshot( 17 | container.innerHTML, 18 | `5` 19 | ); 20 | }); 21 | 22 | test('should accept custom `count` prop', () => { 23 | const { container } = render(() => ); 24 | 25 | assert.snapshot( 26 | container.innerHTML, 27 | `99` 28 | ); 29 | }); 30 | 31 | test('should increment count after `button#incr` click', async () => { 32 | const { container } = render(() => ); 33 | 34 | assert.snapshot( 35 | container.innerHTML, 36 | `5` 37 | ); 38 | 39 | const button = await screen.findByRole( 40 | 'button', { name: '++' } 41 | ) 42 | 43 | assert.ok(isInDom(button)); 44 | fireEvent.click(button); 45 | 46 | assert.snapshot( 47 | container.innerHTML, 48 | `6` 49 | ); 50 | }); 51 | 52 | test('should decrement count after `button#decr` click', async () => { 53 | const { container } = render(() => ); 54 | 55 | assert.snapshot( 56 | container.innerHTML, 57 | `5` 58 | ); 59 | 60 | const button = await screen.findByRole( 61 | 'button', { name: /--/ } 62 | ) 63 | 64 | assert.ok(isInDom(button)); 65 | fireEvent.click(button); 66 | 67 | assert.snapshot( 68 | container.innerHTML, 69 | `4` 70 | ); 71 | }); 72 | 73 | test.run(); 74 | -------------------------------------------------------------------------------- /examples/suites/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "test": "uvu tests" 5 | }, 6 | "devDependencies": { 7 | "uvu": "^0.0.18" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/suites/src/math.js: -------------------------------------------------------------------------------- 1 | exports.sum = (a, b) => a + b; 2 | exports.div = (a, b) => a / b; 3 | exports.mod = (a, b) => a % b; 4 | -------------------------------------------------------------------------------- /examples/suites/src/utils.js: -------------------------------------------------------------------------------- 1 | exports.capitalize = function (str) { 2 | return str[0].toUpperCase() + str.substring(1); 3 | } 4 | 5 | exports.dashify = function (str) { 6 | return str.replace(/([a-zA-Z])(?=[A-Z\d])/g, '$1-').toLowerCase(); 7 | } 8 | -------------------------------------------------------------------------------- /examples/suites/tests/math.js: -------------------------------------------------------------------------------- 1 | const { suite } = require('uvu'); 2 | const assert = require('uvu/assert'); 3 | const math = require('../src/math'); 4 | 5 | const sum = suite('sum'); 6 | 7 | sum('should be a function', () => { 8 | assert.type(math.sum, 'function'); 9 | }); 10 | 11 | sum('should compute values', () => { 12 | assert.is(math.sum(1, 2), 3); 13 | assert.is(math.sum(-1, -2), -3); 14 | assert.is(math.sum(-1, 1), 0); 15 | }); 16 | 17 | sum.run(); 18 | 19 | // --- 20 | 21 | const div = suite('div'); 22 | 23 | div('should be a function', () => { 24 | assert.type(math.div, 'function'); 25 | }); 26 | 27 | div('should compute values', () => { 28 | assert.is(math.div(1, 2), 0.5); 29 | assert.is(math.div(-1, -2), 0.5); 30 | assert.is(math.div(-1, 1), -1); 31 | }); 32 | 33 | div.run(); 34 | 35 | // --- 36 | 37 | const mod = suite('mod'); 38 | 39 | mod('should be a function', () => { 40 | assert.type(math.mod, 'function'); 41 | }); 42 | 43 | mod('should compute values', () => { 44 | assert.is(math.mod(1, 2), 1); 45 | assert.is(math.mod(-3, -2), -1); 46 | assert.is(math.mod(7, 4), 3); 47 | }); 48 | 49 | mod.run(); 50 | -------------------------------------------------------------------------------- /examples/suites/tests/utils.js: -------------------------------------------------------------------------------- 1 | const { suite } = require('uvu'); 2 | const assert = require('uvu/assert'); 3 | const utils = require('../src/utils'); 4 | 5 | const capitalize = suite('capitalize'); 6 | 7 | capitalize('should be a function', () => { 8 | assert.type(utils.capitalize, 'function'); 9 | }); 10 | 11 | capitalize('should capitalize a word', () => { 12 | assert.is(utils.capitalize('hello'), 'Hello'); 13 | }); 14 | 15 | capitalize('should only capitalize the 1st word', () => { 16 | assert.is(utils.capitalize('foo bar'), 'Foo bar'); 17 | }); 18 | 19 | capitalize.run(); 20 | 21 | // --- 22 | 23 | const dashify = suite('dashify'); 24 | 25 | dashify('should be a function', () => { 26 | assert.type(utils.dashify, 'function'); 27 | }); 28 | 29 | dashify('should replace camelCase with dash-case', () => { 30 | assert.is(utils.dashify('fooBar'), 'foo-bar'); 31 | assert.is(utils.dashify('FooBar'), 'foo-bar'); 32 | }); 33 | 34 | dashify('should enforce lowercase', () => { 35 | assert.is(utils.dashify('foobar'), 'foobar'); 36 | }); 37 | 38 | dashify.run(); 39 | -------------------------------------------------------------------------------- /examples/supertest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "test": "uvu tests" 5 | }, 6 | "devDependencies": { 7 | "polka": "0.5.2", 8 | "supertest": "4.0.2", 9 | "uvu": "^0.2.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/supertest/src/index.js: -------------------------------------------------------------------------------- 1 | const polka = require('polka'); 2 | const Users = require('./users'); 3 | const Pets = require('./pets'); 4 | 5 | module.exports = ( 6 | polka() 7 | .use('/pets', Pets) 8 | .use('/users', Users) 9 | .get('/', (req, res) => { 10 | res.setHeader('Content-Type', 'text/plain'); 11 | res.end('OK'); 12 | }) 13 | ); 14 | -------------------------------------------------------------------------------- /examples/supertest/src/pets.js: -------------------------------------------------------------------------------- 1 | const polka = require('polka'); 2 | 3 | module.exports = ( 4 | polka() 5 | .get('/', (req, res) => { 6 | res.end(`GET /pets :: ${req.url}`); 7 | }) 8 | .get('/:id', (req, res) => { 9 | res.end(`GET /pets/:id :: ${req.url}`); 10 | }) 11 | .put('/:id', (req, res) => { 12 | res.statusCode = 201; // why not? 13 | res.end(`PUT /pets/:id :: ${req.url}`); 14 | }) 15 | ); 16 | -------------------------------------------------------------------------------- /examples/supertest/src/users.js: -------------------------------------------------------------------------------- 1 | const polka = require('polka'); 2 | 3 | module.exports = ( 4 | polka() 5 | .get('/', (req, res) => { 6 | res.end(`GET /users :: ${req.url}`); 7 | }) 8 | .get('/:id', (req, res) => { 9 | res.end(`GET /users/:id :: ${req.url}`); 10 | }) 11 | .put('/:id', (req, res) => { 12 | res.statusCode = 201; // why not? 13 | res.end(`PUT /users/:id :: ${req.url}`); 14 | }) 15 | ); 16 | -------------------------------------------------------------------------------- /examples/supertest/tests/index.js: -------------------------------------------------------------------------------- 1 | const { test } = require('uvu'); 2 | const assert = require('uvu/assert'); 3 | const request = require('supertest'); 4 | const App = require('../src'); 5 | 6 | test('should export Polka instance', () => { 7 | assert.is(App.constructor.name, 'Polka'); 8 | assert.type(App.handler, 'function'); 9 | assert.is(App.server, undefined); 10 | 11 | assert.type(App.get, 'function'); 12 | assert.type(App.head, 'function'); 13 | assert.type(App.patch, 'function'); 14 | assert.type(App.connect, 'function'); 15 | assert.type(App.delete, 'function'); 16 | assert.type(App.post, 'function'); 17 | }); 18 | 19 | // Option 1: Use `supertest` assertions 20 | test('should receive "OK" for "GET /" route', async () => { 21 | await request(App.handler) 22 | .get('/') 23 | .expect('Content-Type', /text\/plain/) 24 | .expect(200, 'OK') 25 | }); 26 | 27 | // Option 2: Save `supertest` request; assert directly 28 | test('should receive "OK" for "GET /" route', async () => { 29 | let res = await request(App.handler).get('/'); 30 | assert.is(res.header['content-type'], 'text/plain'); 31 | assert.is(res.status, 200); 32 | assert.is(res.text, 'OK'); 33 | }); 34 | 35 | test.run(); 36 | -------------------------------------------------------------------------------- /examples/supertest/tests/pets.js: -------------------------------------------------------------------------------- 1 | const { test } = require('uvu'); 2 | const request = require('supertest'); 3 | const App = require('../src'); 4 | 5 | /** 6 | * NOTE: File uses `supertest` only 7 | * @see tests/users.js for `uvu/assert` combo. 8 | */ 9 | 10 | test('GET /pets', async () => { 11 | await request(App.handler) 12 | .get('/pets') 13 | .expect(200, 'GET /pets :: /') 14 | }); 15 | 16 | test('GET /pets/:id', async () => { 17 | await request(App.handler) 18 | .get('/pets/123') 19 | .expect(200, `GET /pets/:id :: /123`) 20 | 21 | await request(App.handler) 22 | .get('/pets/dog') 23 | .expect(200, `GET /pets/:id :: /dog`) 24 | }); 25 | 26 | test('PUT /pets/:id', async () => { 27 | await request(App.handler) 28 | .put('/pets/123') 29 | .expect(201, `PUT /pets/:id :: /123`) 30 | 31 | await request(App.handler) 32 | .put('/pets/dog') 33 | .expect(201, `PUT /pets/:id :: /dog`) 34 | }); 35 | 36 | test.run(); 37 | -------------------------------------------------------------------------------- /examples/supertest/tests/users.js: -------------------------------------------------------------------------------- 1 | const { test } = require('uvu'); 2 | const request = require('supertest'); 3 | const assert = require('uvu/assert'); 4 | const App = require('../src'); 5 | 6 | /** 7 | * NOTE: File uses `supertest` w/ `uvu/assert` together 8 | * @see tests/pets.js for `supertest` usage only 9 | */ 10 | 11 | test('GET /users', async () => { 12 | let res = await request(App.handler).get('/users'); 13 | assert.is(res.text, 'GET /users :: /'); 14 | assert.is(res.status, 200); 15 | }); 16 | 17 | test('GET /users/:id', async () => { 18 | let res1 = await request(App.handler).get('/users/123'); 19 | assert.is(res1.text, `GET /users/:id :: /123`); 20 | assert.is(res1.status, 200); 21 | 22 | let res2 = await request(App.handler).get('/users/dog'); 23 | assert.is(res2.text, `GET /users/:id :: /dog`); 24 | assert.is(res2.status, 200); 25 | }); 26 | 27 | test('PUT /users/:id', async () => { 28 | let res1 = await request(App.handler).put('/users/123'); 29 | assert.is(res1.text, `PUT /users/:id :: /123`); 30 | assert.is(res1.status, 201); 31 | 32 | let res2 = await request(App.handler).put('/users/dog'); 33 | assert.is(res2.text, `PUT /users/:id :: /dog`); 34 | assert.is(res2.status, 201); 35 | }); 36 | 37 | test.run(); 38 | -------------------------------------------------------------------------------- /examples/svelte/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "test": "uvu tests -r esm -r tests/setup/register -i setup" 5 | }, 6 | "devDependencies": { 7 | "esm": "3.2.25", 8 | "jsdom": "16.3.0", 9 | "svelte": "3.24.0", 10 | "uvu": "^0.2.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/svelte/src/Count.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | {count} 15 | 16 | -------------------------------------------------------------------------------- /examples/svelte/tests/Count.js: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as ENV from './setup/env'; 4 | 5 | // Relies on `setup/register` 6 | import Count from '../src/Count.svelte'; 7 | 8 | test.before(ENV.setup); 9 | test.before.each(ENV.reset); 10 | 11 | test('should render with "5" by default', () => { 12 | const { container } = ENV.render(Count); 13 | 14 | assert.snapshot( 15 | container.innerHTML, 16 | ` 5 ` 17 | ); 18 | }); 19 | 20 | test('should accept custom `count` prop', () => { 21 | const { container } = ENV.render(Count, { count: 99 }); 22 | 23 | assert.snapshot( 24 | container.innerHTML, 25 | ` 99 ` 26 | ); 27 | }); 28 | 29 | test('should increment count after `button#incr` click', async () => { 30 | const { container } = ENV.render(Count); 31 | 32 | assert.snapshot( 33 | container.innerHTML, 34 | ` 5 ` 35 | ); 36 | 37 | await ENV.fire( 38 | container.querySelector('#incr'), 39 | 'click' 40 | ); 41 | 42 | assert.snapshot( 43 | container.innerHTML, 44 | ` 6 ` 45 | ); 46 | }); 47 | 48 | test('should decrement count after `button#decr` click', async () => { 49 | const { container } = ENV.render(Count); 50 | 51 | assert.snapshot( 52 | container.innerHTML, 53 | ` 5 ` 54 | ); 55 | 56 | await ENV.fire( 57 | container.querySelector('#decr'), 58 | 'click' 59 | ); 60 | 61 | assert.snapshot( 62 | container.innerHTML, 63 | ` 4 ` 64 | ); 65 | }); 66 | 67 | test.run(); 68 | -------------------------------------------------------------------------------- /examples/svelte/tests/setup/env.js: -------------------------------------------------------------------------------- 1 | import { JSDOM } from 'jsdom'; 2 | import { tick } from 'svelte'; 3 | 4 | const { window } = new JSDOM(''); 5 | 6 | export function setup() { 7 | // @ts-ignore 8 | global.window = window; 9 | global.document = window.document; 10 | global.navigator = window.navigator; 11 | global.getComputedStyle = window.getComputedStyle; 12 | global.requestAnimationFrame = null; 13 | } 14 | 15 | export function reset() { 16 | window.document.title = ''; 17 | window.document.head.innerHTML = ''; 18 | window.document.body.innerHTML = ''; 19 | } 20 | 21 | /** 22 | * @typedef RenderOutput 23 | * @property container {HTMLElement} 24 | * @property component {import('svelte').SvelteComponent} 25 | */ 26 | 27 | /** 28 | * @return {RenderOutput} 29 | */ 30 | export function render(Tag, props = {}) { 31 | Tag = Tag.default || Tag; 32 | const container = window.document.body; 33 | const component = new Tag({ props, target: container }); 34 | return { container, component }; 35 | } 36 | 37 | /** 38 | * @param {HTMLElement} elem 39 | * @param {String} event 40 | * @param {any} [details] 41 | * @returns Promise 42 | */ 43 | export function fire(elem, event, details) { 44 | let evt = new window.Event(event, details); 45 | elem.dispatchEvent(evt); 46 | return tick(); 47 | } 48 | -------------------------------------------------------------------------------- /examples/svelte/tests/setup/register.js: -------------------------------------------------------------------------------- 1 | const { parse } = require('path'); 2 | const { compile } = require('svelte/compiler'); 3 | 4 | function transform(hook, source, filename) { 5 | const { name } = parse(filename); 6 | 7 | const { js, warnings } = compile(source, { 8 | name: name[0].toUpperCase() + name.substring(1), 9 | format: 'cjs', 10 | filename, 11 | }); 12 | 13 | warnings.forEach(warning => { 14 | console.warn(`\nSvelte Warning in ${warning.filename}:`); 15 | console.warn(warning.message); 16 | console.warn(warning.frame); 17 | }); 18 | 19 | return hook(js.code, filename); 20 | } 21 | 22 | const loadJS = require.extensions['.js']; 23 | 24 | // Runtime DOM hook for require("*.svelte") files 25 | // Note: for SSR/Node.js hook, use `svelte/register` 26 | require.extensions['.svelte'] = function (mod, filename) { 27 | const orig = mod._compile.bind(mod); 28 | mod._compile = code => transform(orig, code, filename); 29 | loadJS(mod, filename); 30 | } 31 | -------------------------------------------------------------------------------- /examples/typescript.module/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /examples/typescript.module/loadr.mjs: -------------------------------------------------------------------------------- 1 | // NOTE: used for "test:alt" script only 2 | // @see https://github.com/lukeed/loadr 3 | export const loaders = ['tsm']; 4 | -------------------------------------------------------------------------------- /examples/typescript.module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "test": "tsm node_modules/uvu/bin.js tests", 6 | "test:alt": "loadr -- uvu tests" 7 | }, 8 | "devDependencies": { 9 | "loadr": "^0.1.1", 10 | "tsm": "^2.0.0", 11 | "uvu": "^0.5.1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/typescript.module/src/math.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {number} a 3 | * @param {number} b 4 | */ 5 | export const sum = (a, b) => a + b; 6 | 7 | /** 8 | * @param {number} a 9 | * @param {number} b 10 | */ 11 | export const div = (a, b) => a / b; 12 | 13 | /** 14 | * @param {number} a 15 | * @param {number} b 16 | */ 17 | export const mod = (a, b) => a % b; 18 | -------------------------------------------------------------------------------- /examples/typescript.module/src/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} str 3 | */ 4 | export function capitalize(str) { 5 | return str[0].toUpperCase() + str.substring(1); 6 | } 7 | 8 | /** 9 | * @param {string} str 10 | */ 11 | export function dashify(str) { 12 | return str.replace(/([a-zA-Z])(?=[A-Z\d])/g, '$1-').toLowerCase(); 13 | } 14 | -------------------------------------------------------------------------------- /examples/typescript.module/tests/math.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as math from '../src/math.js'; 4 | 5 | test('sum', () => { 6 | assert.type(math.sum, 'function'); 7 | assert.is(math.sum(1, 2), 3); 8 | assert.is(math.sum(-1, -2), -3); 9 | assert.is(math.sum(-1, 1), 0); 10 | }); 11 | 12 | test('div', () => { 13 | assert.type(math.div, 'function'); 14 | assert.is(math.div(1, 2), 0.5); 15 | assert.is(math.div(-1, -2), 0.5); 16 | assert.is(math.div(-1, 1), -1); 17 | }); 18 | 19 | test('mod', () => { 20 | assert.type(math.mod, 'function'); 21 | assert.is(math.mod(1, 2), 1); 22 | assert.is(math.mod(-3, -2), -1); 23 | assert.is(math.mod(7, 4), 3); 24 | }); 25 | 26 | test.run(); 27 | -------------------------------------------------------------------------------- /examples/typescript.module/tests/utils.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as utils from '../src/utils.js'; 4 | 5 | test('capitalize', () => { 6 | assert.type(utils.capitalize, 'function'); 7 | assert.is(utils.capitalize('hello'), 'Hello'); 8 | assert.is(utils.capitalize('foo bar'), 'Foo bar'); 9 | }); 10 | 11 | test('dashify', () => { 12 | assert.type(utils.dashify, 'function'); 13 | assert.is(utils.dashify('fooBar'), 'foo-bar'); 14 | assert.is(utils.dashify('FooBar'), 'foo-bar'); 15 | assert.is(utils.dashify('foobar'), 'foobar'); 16 | }); 17 | 18 | test.run(); 19 | -------------------------------------------------------------------------------- /examples/typescript.module/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "allowJs": true, 5 | "target": "esnext", 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "forceConsistentCasingInFileNames": true, 9 | "alwaysStrict": true, 10 | "module": "esnext", 11 | "checkJs": true, 12 | "noEmit": true 13 | }, 14 | "include": [ 15 | "src", 16 | "tests" 17 | ], 18 | "exclude": [ 19 | "node_modules" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /examples/typescript/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /examples/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "test": "uvu -r tsm tests" 5 | }, 6 | "devDependencies": { 7 | "tsm": "^2.0.0", 8 | "uvu": "^0.5.1" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/typescript/src/math.ts: -------------------------------------------------------------------------------- 1 | export function sum(a: number, b: number): number { 2 | return a + b; 3 | } 4 | 5 | export function div(a: number, b: number): number { 6 | return a / b; 7 | } 8 | 9 | export function mod(a: number, b: number): number { 10 | return a % b; 11 | } 12 | -------------------------------------------------------------------------------- /examples/typescript/src/utils.ts: -------------------------------------------------------------------------------- 1 | export function capitalize(str: string): string { 2 | return str[0].toUpperCase() + str.substring(1); 3 | } 4 | 5 | export function dashify(str: string): string { 6 | return str.replace(/([a-zA-Z])(?=[A-Z\d])/g, '$1-').toLowerCase(); 7 | } 8 | -------------------------------------------------------------------------------- /examples/typescript/tests/math.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as math from '../src/math'; 4 | 5 | test('sum', () => { 6 | assert.type(math.sum, 'function'); 7 | assert.is(math.sum(1, 2), 3); 8 | assert.is(math.sum(-1, -2), -3); 9 | assert.is(math.sum(-1, 1), 0); 10 | }); 11 | 12 | test('div', () => { 13 | assert.type(math.div, 'function'); 14 | assert.is(math.div(1, 2), 0.5); 15 | assert.is(math.div(-1, -2), 0.5); 16 | assert.is(math.div(-1, 1), -1); 17 | }); 18 | 19 | test('mod', () => { 20 | assert.type(math.mod, 'function'); 21 | assert.is(math.mod(1, 2), 1); 22 | assert.is(math.mod(-3, -2), -1); 23 | assert.is(math.mod(7, 4), 3); 24 | }); 25 | 26 | test.run(); 27 | -------------------------------------------------------------------------------- /examples/typescript/tests/utils.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as utils from '../src/utils'; 4 | 5 | test('capitalize', () => { 6 | assert.type(utils.capitalize, 'function'); 7 | assert.is(utils.capitalize('hello'), 'Hello'); 8 | assert.is(utils.capitalize('foo bar'), 'Foo bar'); 9 | }); 10 | 11 | test('dashify', () => { 12 | assert.type(utils.dashify, 'function'); 13 | assert.is(utils.dashify('fooBar'), 'foo-bar'); 14 | assert.is(utils.dashify('FooBar'), 'foo-bar'); 15 | assert.is(utils.dashify('foobar'), 'foobar'); 16 | }); 17 | 18 | test.run(); 19 | -------------------------------------------------------------------------------- /examples/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "allowJs": true, 5 | "target": "esnext", 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "forceConsistentCasingInFileNames": true, 9 | "alwaysStrict": true, 10 | "module": "esnext", 11 | "checkJs": true, 12 | "noEmit": true 13 | }, 14 | "include": [ 15 | "src", 16 | "tests" 17 | ], 18 | "exclude": [ 19 | "node_modules" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /examples/watch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "test": "uvu tests", 5 | "watch": "watchlist src tests -- yarn test" 6 | }, 7 | "devDependencies": { 8 | "uvu": "^0.2.0", 9 | "watchlist": "^0.2.1" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/watch/src/math.js: -------------------------------------------------------------------------------- 1 | exports.sum = (a, b) => a + b; 2 | exports.div = (a, b) => a / b; 3 | exports.mod = (a, b) => a % b; 4 | -------------------------------------------------------------------------------- /examples/watch/src/utils.js: -------------------------------------------------------------------------------- 1 | exports.capitalize = function (str) { 2 | return str[0].toUpperCase() + str.substring(1); 3 | } 4 | 5 | exports.dashify = function (str) { 6 | return str.replace(/([a-zA-Z])(?=[A-Z\d])/g, '$1-').toLowerCase(); 7 | } 8 | -------------------------------------------------------------------------------- /examples/watch/tests/math.js: -------------------------------------------------------------------------------- 1 | const { suite } = require('uvu'); 2 | const assert = require('uvu/assert'); 3 | const math = require('../src/math'); 4 | 5 | const sum = suite('sum'); 6 | 7 | sum('should be a function', () => { 8 | assert.type(math.sum, 'function'); 9 | }); 10 | 11 | sum('should compute values', () => { 12 | assert.is(math.sum(1, 2), 3); 13 | assert.is(math.sum(-1, -2), -3); 14 | assert.is(math.sum(-1, 1), 0); 15 | }); 16 | 17 | sum.run(); 18 | 19 | // --- 20 | 21 | const div = suite('div'); 22 | 23 | div('should be a function', () => { 24 | assert.type(math.div, 'function'); 25 | }); 26 | 27 | div('should compute values', () => { 28 | assert.is(math.div(1, 2), 0.5); 29 | assert.is(math.div(-1, -2), 0.5); 30 | assert.is(math.div(-1, 1), -1); 31 | }); 32 | 33 | div.run(); 34 | 35 | // --- 36 | 37 | const mod = suite('mod'); 38 | 39 | mod('should be a function', () => { 40 | assert.type(math.mod, 'function'); 41 | }); 42 | 43 | mod('should compute values', () => { 44 | assert.is(math.mod(1, 2), 1); 45 | assert.is(math.mod(-3, -2), -1); 46 | assert.is(math.mod(7, 4), 3); 47 | }); 48 | 49 | mod.run(); 50 | -------------------------------------------------------------------------------- /examples/watch/tests/utils.js: -------------------------------------------------------------------------------- 1 | const { suite } = require('uvu'); 2 | const assert = require('uvu/assert'); 3 | const utils = require('../src/utils'); 4 | 5 | const capitalize = suite('capitalize'); 6 | 7 | capitalize('should be a function', () => { 8 | assert.type(utils.capitalize, 'function'); 9 | }); 10 | 11 | capitalize('should capitalize a word', () => { 12 | assert.is(utils.capitalize('hello'), 'Hello'); 13 | }); 14 | 15 | capitalize('should only capitalize the 1st word', () => { 16 | assert.is(utils.capitalize('foo bar'), 'Foo bar'); 17 | }); 18 | 19 | capitalize.run(); 20 | 21 | // --- 22 | 23 | const dashify = suite('dashify'); 24 | 25 | dashify('should be a function', () => { 26 | assert.type(utils.dashify, 'function'); 27 | }); 28 | 29 | dashify('should replace camelCase with dash-case', () => { 30 | assert.is(utils.dashify('fooBar'), 'foo-bar'); 31 | assert.is(utils.dashify('FooBar'), 'foo-bar'); 32 | }); 33 | 34 | dashify('should enforce lowercase', () => { 35 | assert.is(utils.dashify('foobar'), 'foobar'); 36 | }); 37 | 38 | dashify.run(); 39 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Luke Edwards (lukeed.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uvu", 3 | "version": "0.5.6", 4 | "repository": "lukeed/uvu", 5 | "description": "uvu is an extremely fast and lightweight test runner for Node.js and the browser", 6 | "module": "dist/index.mjs", 7 | "main": "dist/index.js", 8 | "types": "index.d.ts", 9 | "license": "MIT", 10 | "bin": { 11 | "uvu": "bin.js" 12 | }, 13 | "exports": { 14 | ".": { 15 | "types": "./index.d.ts", 16 | "require": "./dist/index.js", 17 | "import": "./dist/index.mjs" 18 | }, 19 | "./assert": { 20 | "types": "./assert/index.d.ts", 21 | "require": "./assert/index.js", 22 | "import": "./assert/index.mjs" 23 | }, 24 | "./diff": { 25 | "types": "./diff/index.d.ts", 26 | "require": "./diff/index.js", 27 | "import": "./diff/index.mjs" 28 | }, 29 | "./parse": { 30 | "types": "./parse/index.d.ts", 31 | "require": "./parse/index.js", 32 | "import": "./parse/index.mjs" 33 | }, 34 | "./run": { 35 | "types": "./run/index.d.ts", 36 | "require": "./run/index.js", 37 | "import": "./run/index.mjs" 38 | } 39 | }, 40 | "files": [ 41 | "*.js", 42 | "*.d.ts", 43 | "assert", 44 | "parse", 45 | "diff", 46 | "dist", 47 | "run" 48 | ], 49 | "modes": { 50 | "diff": "src/diff.js", 51 | "assert": "src/assert.js", 52 | "default": "src/index.js" 53 | }, 54 | "scripts": { 55 | "build": "bundt", 56 | "test": "node test" 57 | }, 58 | "engines": { 59 | "node": ">=8" 60 | }, 61 | "keywords": [ 62 | "assert", 63 | "diffs", 64 | "runner", 65 | "snapshot", 66 | "test" 67 | ], 68 | "dependencies": { 69 | "dequal": "^2.0.0", 70 | "diff": "^5.0.0", 71 | "kleur": "^4.0.3", 72 | "sade": "^1.7.3" 73 | }, 74 | "devDependencies": { 75 | "bundt": "1.1.1", 76 | "esm": "3.2.25", 77 | "module-alias": "2.2.2", 78 | "totalist": "2.0.0" 79 | }, 80 | "_moduleAliases": { 81 | "uvu": "src/index.js", 82 | "uvu/diff": "src/diff.js", 83 | "uvu/assert": "src/assert.js" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /parse/index.d.ts: -------------------------------------------------------------------------------- 1 | type Arrayable = T[] | T; 2 | 3 | export interface Suite { 4 | /** The relative file path */ 5 | name: string; 6 | /** The absolute file path */ 7 | file: string; 8 | } 9 | 10 | export interface Options { 11 | cwd: string; 12 | require: Arrayable; 13 | ignore: Arrayable; 14 | } 15 | 16 | export interface Argv { 17 | dir: string; 18 | suites: Suite[]; 19 | requires: boolean; 20 | } 21 | 22 | export function parse(dir?: string, pattern?: string|RegExp, opts?: Partial): Promise; 23 | -------------------------------------------------------------------------------- /parse/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { readdir, stat } = require('fs'); 3 | const { resolve, join } = require('path'); 4 | const { promisify } = require('util'); 5 | 6 | const ls = promisify(readdir); 7 | const toStat = promisify(stat); 8 | const toRegex = x => new RegExp(x, 'i'); 9 | 10 | async function parse(dir, pattern, opts = {}) { 11 | if (pattern) pattern = toRegex(pattern); 12 | else if (dir) pattern = /(((?:[^\/]*(?:\/|$))*)[\\\/])?\w+\.([mc]js|[jt]sx?)$/; 13 | else pattern = /((\/|^)(tests?|__tests?__)\/.*|\.(tests?|spec)|^\/?tests?)\.([mc]js|[jt]sx?)$/i; 14 | dir = resolve(opts.cwd || '.', dir || '.'); 15 | 16 | let suites = []; 17 | let requires = [].concat(opts.require || []).filter(Boolean); 18 | let ignores = ['^.git', 'node_modules'].concat(opts.ignore || []).map(toRegex); 19 | 20 | requires.forEach(name => { 21 | try { return require(name) } 22 | catch (e) { throw new Error(`Cannot find module "${name}"`) } 23 | }); 24 | 25 | // NOTE: Node 8.x support 26 | // @modified lukeed/totalist 27 | await (async function collect(d, p) { 28 | await ls(d).then(files => { 29 | return Promise.all( 30 | files.map(async str => { 31 | let name = join(p, str); 32 | for (let i = ignores.length; i--;) { 33 | if (ignores[i].test(name)) return; 34 | } 35 | 36 | let file = join(d, str); 37 | let stats = await toStat(file); 38 | if (stats.isDirectory()) return collect(file, name); 39 | else if (pattern.test(name)) suites.push({ name, file }); 40 | }) 41 | ); 42 | }); 43 | })(dir, ''); 44 | 45 | suites.sort((a, b) => a.name.localeCompare(b.name)); 46 | 47 | return { dir, suites, requires: requires.length > 0 }; 48 | } 49 | 50 | exports.parse = parse; 51 | -------------------------------------------------------------------------------- /parse/index.mjs: -------------------------------------------------------------------------------- 1 | import { readdir, stat } from 'fs'; 2 | import { createRequire } from 'module'; 3 | import { resolve, join } from 'path'; 4 | import { promisify } from 'util'; 5 | 6 | const ls = promisify(readdir); 7 | const toStat = promisify(stat); 8 | const toRegex = x => new RegExp(x, 'i'); 9 | 10 | export async function parse(dir, pattern, opts = {}) { 11 | if (pattern) pattern = toRegex(pattern); 12 | else if (dir) pattern = /(((?:[^\/]*(?:\/|$))*)[\\\/])?\w+\.([mc]js|[jt]sx?)$/; 13 | else pattern = /((\/|^)(tests?|__tests?__)\/.*|\.(tests?|spec)|^\/?tests?)\.([mc]js|[jt]sx?)$/i; 14 | dir = resolve(opts.cwd || '.', dir || '.'); 15 | 16 | let suites = []; 17 | let requires = [].concat(opts.require || []).filter(Boolean); 18 | let ignores = ['^.git', 'node_modules'].concat(opts.ignore || []).map(toRegex); 19 | 20 | if (requires.length) { 21 | let $require = createRequire(import.meta.url); 22 | requires.forEach(name => { 23 | try { return $require(name) } 24 | catch (e) { throw new Error(`Cannot find module "${name}"`) } 25 | }); 26 | } 27 | 28 | // NOTE: Node 8.x support 29 | // @modified lukeed/totalist 30 | await (async function collect(d, p) { 31 | await ls(d).then(files => { 32 | return Promise.all( 33 | files.map(async str => { 34 | let name = join(p, str); 35 | for (let i = ignores.length; i--;) { 36 | if (ignores[i].test(name)) return; 37 | } 38 | 39 | let file = join(d, str); 40 | let stats = await toStat(file); 41 | if (stats.isDirectory()) return collect(file, name); 42 | else if (pattern.test(name)) suites.push({ name, file }); 43 | }) 44 | ); 45 | }); 46 | })(dir, ''); 47 | 48 | suites.sort((a, b) => a.name.localeCompare(b.name)); 49 | 50 | return { dir, suites, requires: requires.length > 0 }; 51 | } 52 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
2 | uvu 3 |
4 | 5 | 19 | 20 |
21 | uvu is an extremely fast and lightweight test runner for Node.js and the browser
22 | Ultimate Velocity, Unleashed

23 | example with suites 24 |
25 | 26 | 27 | ## Features 28 | 29 | * Super [lightweight](https://npm.anvaka.com/#/view/2d/uvu) 30 | * Extremely [performant](#benchmarks) 31 | * Individually executable test files 32 | * Supports `async`/`await` tests 33 | * Supports native ES Modules 34 | * Browser-Compatible 35 | * Familiar API 36 | 37 | 38 | ## Install 39 | 40 | ``` 41 | $ npm install --save-dev uvu 42 | ``` 43 | 44 | 45 | ## Usage 46 | 47 | > Check out [`/examples`](/examples) for a list of working demos! 48 | 49 | ```js 50 | // tests/demo.js 51 | import { test } from 'uvu'; 52 | import * as assert from 'uvu/assert'; 53 | 54 | test('Math.sqrt()', () => { 55 | assert.is(Math.sqrt(4), 2); 56 | assert.is(Math.sqrt(144), 12); 57 | assert.is(Math.sqrt(2), Math.SQRT2); 58 | }); 59 | 60 | test('JSON', () => { 61 | const input = { 62 | foo: 'hello', 63 | bar: 'world' 64 | }; 65 | 66 | const output = JSON.stringify(input); 67 | 68 | assert.snapshot(output, `{"foo":"hello","bar":"world"}`); 69 | assert.equal(JSON.parse(output), input, 'matches original'); 70 | }); 71 | 72 | test.run(); 73 | ``` 74 | 75 | Then execute this test file: 76 | 77 | ```sh 78 | # via `uvu` cli, for all `/tests/**` files 79 | $ uvu -r esm tests 80 | 81 | # via `node` directly, for file isolation 82 | $ node -r esm tests/demo.js 83 | ``` 84 | 85 | > **Note:** The `-r esm` is for legacy Node.js versions. [Learn More](/docs/esm.md) 86 | 87 | > [View the `uvu` CLI documentation](/docs/cli.md) 88 | 89 | 90 | ## Assertions 91 | 92 | The [`uvu/assert`](/docs/api.assert.md) module is _completely_ optional. 93 | 94 | In fact, you may use any assertion library, including Node's native [`assert`](https://nodejs.org/api/assert.html) module! This works because `uvu` relies on thrown Errors to detect failures. Implicitly, this also means that any uncaught exceptions and/or unhandled `Promise` rejections will result in a failure, which is what you want! 95 | 96 | 97 | ## API 98 | 99 | ### Module: `uvu` 100 | 101 | > [View `uvu` API documentation](/docs/api.uvu.md) 102 | 103 | The main entry from which you will import the `test` or `suite` methods. 104 | 105 | ### Module: `uvu/assert` 106 | 107 | > [View `uvu/assert` API documentation](/docs/api.assert.md) 108 | 109 | A collection of assertion methods to use within your tests. Please note that: 110 | 111 | * these are browser compatible 112 | * these are _completely_ optional 113 | 114 | 115 | ## Benchmarks 116 | 117 | > via the [`/bench`](/bench) directory with Node v10.21.0 118 | 119 | Below you'll find each test runner with two timing values: 120 | 121 | * the `took ___` value is the total process execution time – from startup to termination 122 | * the parenthesis value (`(___)`) is the self-reported execution time, if known 123 | 124 | Each test runner's `stdout` is printed to the console to verify all assertions pass.
Said output is excluded below for brevity. 125 | 126 | ``` 127 | ~> "ava" took 594ms ( ??? ) 128 | ~> "jest" took 962ms (356 ms) 129 | ~> "mocha" took 209ms ( 4 ms) 130 | ~> "tape" took 122ms ( ??? ) 131 | ~> "uvu" took 72ms ( 1.3ms) 132 | ``` 133 | 134 | 135 | ## License 136 | 137 | MIT © [Luke Edwards](https://lukeed.com) 138 | -------------------------------------------------------------------------------- /run/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { Suite } from 'uvu/parse'; 2 | export function run(suites: Suite[], options?: { bail: boolean }): Promise; 3 | -------------------------------------------------------------------------------- /run/index.js: -------------------------------------------------------------------------------- 1 | exports.run = async function (suites, opts={}) { 2 | globalThis.UVU_DEFER = 1; 3 | const uvu = require('uvu'); 4 | 5 | suites.forEach((suite, idx) => { 6 | globalThis.UVU_QUEUE.push([suite.name]); 7 | globalThis.UVU_INDEX = idx; 8 | require(suite.file); 9 | }); 10 | 11 | await uvu.exec(opts.bail); 12 | } 13 | -------------------------------------------------------------------------------- /run/index.mjs: -------------------------------------------------------------------------------- 1 | export async function run(suites, opts={}) { 2 | globalThis.UVU_DEFER = 1; 3 | const uvu = await import('uvu'); 4 | 5 | let suite, idx=0; 6 | for (suite of suites) { 7 | globalThis.UVU_INDEX = idx++; 8 | globalThis.UVU_QUEUE.push([suite.name]); 9 | await import('file:///' + suite.file); 10 | } 11 | 12 | await uvu.exec(opts.bail); 13 | } 14 | -------------------------------------------------------------------------------- /shots/suites.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeed/uvu/9419247b8f93b61ce9a3aeca562f08491101a765/shots/suites.gif -------------------------------------------------------------------------------- /shots/uvu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeed/uvu/9419247b8f93b61ce9a3aeca562f08491101a765/shots/uvu.jpg -------------------------------------------------------------------------------- /src/assert.d.ts: -------------------------------------------------------------------------------- 1 | type Types = 'string' | 'number' | 'boolean' | 'object' | 'undefined' | 'function'; 2 | 3 | export type Message = string | Error; 4 | export function ok(actual: any, msg?: Message): asserts actual; 5 | export function is(actual: Actual, expected: Expects, msg?: Message): void; 6 | export function equal(actual: Actual, expected: Expects, msg?: Message): void; 7 | export function equal(actual: Actual, expected: Expects, msg?: Message): void; 8 | export function equal(actual: Actual, expected: Expects, msg?: Message): void; 9 | export function type(actual: any, expects: Types, msg?: Message): void; 10 | export function instance(actual: any, expects: any, msg?: Message): void; 11 | export function snapshot(actual: string, expects: string, msg?: Message): void; 12 | export function fixture(actual: string, expects: string, msg?: Message): void; 13 | export function match(actual: string, expects: string | RegExp, msg?: Message): void; 14 | export function throws(fn: Function, expects?: Message | RegExp | Function, msg?: Message): void; 15 | export function not(actual: any, msg?: Message): void; 16 | export function unreachable(msg?: Message): void; 17 | 18 | export namespace is { 19 | function not(actual: any, expects: any, msg?: Message): void; 20 | } 21 | 22 | export namespace not { 23 | function ok(actual: any, msg?: Message): void; 24 | function equal(actual: any, expects: any, msg?: Message): void; 25 | function type(actual: any, expects: Types, msg?: Message): void; 26 | function instance(actual: any, expects: any, msg?: Message): void; 27 | function snapshot(actual: string, expects: string, msg?: Message): void; 28 | function fixture(actual: string, expects: string, msg?: Message): void; 29 | function match(actual: string, expects: string | RegExp, msg?: Message): void; 30 | function throws(fn: Function, expects?: Message | RegExp | Function, msg?: Message): void; 31 | } 32 | 33 | export class Assertion extends Error { 34 | name: 'Assertion'; 35 | code: 'ERR_ASSERTION'; 36 | details: false | string; 37 | generated: boolean; 38 | operator: string; 39 | expects: any; 40 | actual: any; 41 | constructor(options?: { 42 | message: string; 43 | details?: string; 44 | generated?: boolean; 45 | operator: string; 46 | expects: any; 47 | actual: any; 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /src/assert.js: -------------------------------------------------------------------------------- 1 | import { dequal } from 'dequal'; 2 | import { compare, lines } from 'uvu/diff'; 3 | 4 | function dedent(str) { 5 | str = str.replace(/\r?\n/g, '\n'); 6 | let arr = str.match(/^[ \t]*(?=\S)/gm); 7 | let i = 0, min = 1/0, len = (arr||[]).length; 8 | for (; i < len; i++) min = Math.min(min, arr[i].length); 9 | return len && min ? str.replace(new RegExp(`^[ \\t]{${min}}`, 'gm'), '') : str; 10 | } 11 | 12 | export class Assertion extends Error { 13 | constructor(opts={}) { 14 | super(opts.message); 15 | this.name = 'Assertion'; 16 | this.code = 'ERR_ASSERTION'; 17 | if (Error.captureStackTrace) { 18 | Error.captureStackTrace(this, this.constructor); 19 | } 20 | this.details = opts.details || false; 21 | this.generated = !!opts.generated; 22 | this.operator = opts.operator; 23 | this.expects = opts.expects; 24 | this.actual = opts.actual; 25 | } 26 | } 27 | 28 | function assert(bool, actual, expects, operator, detailer, backup, msg) { 29 | if (bool) return; 30 | let message = msg || backup; 31 | if (msg instanceof Error) throw msg; 32 | let details = detailer && detailer(actual, expects); 33 | throw new Assertion({ actual, expects, operator, message, details, generated: !msg }); 34 | } 35 | 36 | export function ok(val, msg) { 37 | assert(!!val, false, true, 'ok', false, 'Expected value to be truthy', msg); 38 | } 39 | 40 | export function is(val, exp, msg) { 41 | assert(val === exp, val, exp, 'is', compare, 'Expected values to be strictly equal:', msg); 42 | } 43 | 44 | export function equal(val, exp, msg) { 45 | assert(dequal(val, exp), val, exp, 'equal', compare, 'Expected values to be deeply equal:', msg); 46 | } 47 | 48 | export function unreachable(msg) { 49 | assert(false, true, false, 'unreachable', false, 'Expected not to be reached!', msg); 50 | } 51 | 52 | export function type(val, exp, msg) { 53 | let tmp = typeof val; 54 | assert(tmp === exp, tmp, exp, 'type', false, `Expected "${tmp}" to be "${exp}"`, msg); 55 | } 56 | 57 | export function instance(val, exp, msg) { 58 | let name = '`' + (exp.name || exp.constructor.name) + '`'; 59 | assert(val instanceof exp, val, exp, 'instance', false, `Expected value to be an instance of ${name}`, msg); 60 | } 61 | 62 | export function match(val, exp, msg) { 63 | if (typeof exp === 'string') { 64 | assert(val.includes(exp), val, exp, 'match', false, `Expected value to include "${exp}" substring`, msg); 65 | } else { 66 | assert(exp.test(val), val, exp, 'match', false, `Expected value to match \`${String(exp)}\` pattern`, msg); 67 | } 68 | } 69 | 70 | export function snapshot(val, exp, msg) { 71 | val=dedent(val); exp=dedent(exp); 72 | assert(val === exp, val, exp, 'snapshot', lines, 'Expected value to match snapshot:', msg); 73 | } 74 | 75 | const lineNums = (x, y) => lines(x, y, 1); 76 | export function fixture(val, exp, msg) { 77 | val=dedent(val); exp=dedent(exp); 78 | assert(val === exp, val, exp, 'fixture', lineNums, 'Expected value to match fixture:', msg); 79 | } 80 | 81 | export function throws(blk, exp, msg) { 82 | if (!msg && typeof exp === 'string') { 83 | msg = exp; exp = null; 84 | } 85 | 86 | try { 87 | blk(); 88 | assert(false, false, true, 'throws', false, 'Expected function to throw', msg); 89 | } catch (err) { 90 | if (err instanceof Assertion) throw err; 91 | 92 | if (typeof exp === 'function') { 93 | assert(exp(err), false, true, 'throws', false, 'Expected function to throw matching exception', msg); 94 | } else if (exp instanceof RegExp) { 95 | assert(exp.test(err.message), false, true, 'throws', false, `Expected function to throw exception matching \`${String(exp)}\` pattern`, msg); 96 | } 97 | } 98 | } 99 | 100 | // --- 101 | 102 | export function not(val, msg) { 103 | assert(!val, true, false, 'not', false, 'Expected value to be falsey', msg); 104 | } 105 | 106 | not.ok = not; 107 | 108 | is.not = function (val, exp, msg) { 109 | assert(val !== exp, val, exp, 'is.not', false, 'Expected values not to be strictly equal', msg); 110 | } 111 | 112 | not.equal = function (val, exp, msg) { 113 | assert(!dequal(val, exp), val, exp, 'not.equal', false, 'Expected values not to be deeply equal', msg); 114 | } 115 | 116 | not.type = function (val, exp, msg) { 117 | let tmp = typeof val; 118 | assert(tmp !== exp, tmp, exp, 'not.type', false, `Expected "${tmp}" not to be "${exp}"`, msg); 119 | } 120 | 121 | not.instance = function (val, exp, msg) { 122 | let name = '`' + (exp.name || exp.constructor.name) + '`'; 123 | assert(!(val instanceof exp), val, exp, 'not.instance', false, `Expected value not to be an instance of ${name}`, msg); 124 | } 125 | 126 | not.snapshot = function (val, exp, msg) { 127 | val=dedent(val); exp=dedent(exp); 128 | assert(val !== exp, val, exp, 'not.snapshot', false, 'Expected value not to match snapshot', msg); 129 | } 130 | 131 | not.fixture = function (val, exp, msg) { 132 | val=dedent(val); exp=dedent(exp); 133 | assert(val !== exp, val, exp, 'not.fixture', false, 'Expected value not to match fixture', msg); 134 | } 135 | 136 | not.match = function (val, exp, msg) { 137 | if (typeof exp === 'string') { 138 | assert(!val.includes(exp), val, exp, 'not.match', false, `Expected value not to include "${exp}" substring`, msg); 139 | } else { 140 | assert(!exp.test(val), val, exp, 'not.match', false, `Expected value not to match \`${String(exp)}\` pattern`, msg); 141 | } 142 | } 143 | 144 | not.throws = function (blk, exp, msg) { 145 | if (!msg && typeof exp === 'string') { 146 | msg = exp; exp = null; 147 | } 148 | 149 | try { 150 | blk(); 151 | } catch (err) { 152 | if (typeof exp === 'function') { 153 | assert(!exp(err), true, false, 'not.throws', false, 'Expected function not to throw matching exception', msg); 154 | } else if (exp instanceof RegExp) { 155 | assert(!exp.test(err.message), true, false, 'not.throws', false, `Expected function not to throw exception matching \`${String(exp)}\` pattern`, msg); 156 | } else if (!exp) { 157 | assert(false, true, false, 'not.throws', false, 'Expected function not to throw', msg); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/diff.d.ts: -------------------------------------------------------------------------------- 1 | export function chars(input: any, expects: any): string; 2 | export function lines(input: any, expects: any, linenum?: number): string; 3 | export function direct(input: any, expects: any, lenA?: number, lenB?: number): string; 4 | export function compare(input: any, expects: any): string; 5 | export function arrays(input: any, expects: any): string; 6 | -------------------------------------------------------------------------------- /src/diff.js: -------------------------------------------------------------------------------- 1 | import kleur from 'kleur'; 2 | import * as diff from 'diff'; 3 | 4 | const colors = { 5 | '--': kleur.red, 6 | '··': kleur.grey, 7 | '++': kleur.green, 8 | }; 9 | 10 | const TITLE = kleur.dim().italic; 11 | const TAB=kleur.dim('→'), SPACE=kleur.dim('·'), NL=kleur.dim('↵'); 12 | const LOG = (sym, str) => colors[sym](sym + PRETTY(str)) + '\n'; 13 | const LINE = (num, x) => kleur.dim('L' + String(num).padStart(x, '0') + ' '); 14 | const PRETTY = str => str.replace(/[ ]/g, SPACE).replace(/\t/g, TAB).replace(/(\r?\n)/g, NL); 15 | 16 | function line(obj, prev, pad) { 17 | let char = obj.removed ? '--' : obj.added ? '++' : '··'; 18 | let arr = obj.value.replace(/\r?\n$/, '').split('\n'); 19 | let i=0, tmp, out=''; 20 | 21 | if (obj.added) out += colors[char]().underline(TITLE('Expected:')) + '\n'; 22 | else if (obj.removed) out += colors[char]().underline(TITLE('Actual:')) + '\n'; 23 | 24 | for (; i < arr.length; i++) { 25 | tmp = arr[i]; 26 | if (tmp != null) { 27 | if (prev) out += LINE(prev + i, pad); 28 | out += LOG(char, tmp || '\n'); 29 | } 30 | } 31 | 32 | return out; 33 | } 34 | 35 | // TODO: want better diffing 36 | //~> complex items bail outright 37 | export function arrays(input, expect) { 38 | let arr = diff.diffArrays(input, expect); 39 | let i=0, j=0, k=0, tmp, val, char, isObj, str; 40 | let out = LOG('··', '['); 41 | 42 | for (; i < arr.length; i++) { 43 | char = (tmp = arr[i]).removed ? '--' : tmp.added ? '++' : '··'; 44 | 45 | if (tmp.added) { 46 | out += colors[char]().underline(TITLE('Expected:')) + '\n'; 47 | } else if (tmp.removed) { 48 | out += colors[char]().underline(TITLE('Actual:')) + '\n'; 49 | } 50 | 51 | for (j=0; j < tmp.value.length; j++) { 52 | isObj = (tmp.value[j] && typeof tmp.value[j] === 'object'); 53 | val = stringify(tmp.value[j]).split(/\r?\n/g); 54 | for (k=0; k < val.length;) { 55 | str = ' ' + val[k++] + (isObj ? '' : ','); 56 | if (isObj && k === val.length && (j + 1) < tmp.value.length) str += ','; 57 | out += LOG(char, str); 58 | } 59 | } 60 | } 61 | 62 | return out + LOG('··', ']'); 63 | } 64 | 65 | export function lines(input, expect, linenum = 0) { 66 | let i=0, tmp, output=''; 67 | let arr = diff.diffLines(input, expect); 68 | let pad = String(expect.split(/\r?\n/g).length - linenum).length; 69 | 70 | for (; i < arr.length; i++) { 71 | output += line(tmp = arr[i], linenum, pad); 72 | if (linenum && !tmp.removed) linenum += tmp.count; 73 | } 74 | 75 | return output; 76 | } 77 | 78 | export function chars(input, expect) { 79 | let arr = diff.diffChars(input, expect); 80 | let i=0, output='', tmp; 81 | 82 | let l1 = input.length; 83 | let l2 = expect.length; 84 | 85 | let p1 = PRETTY(input); 86 | let p2 = PRETTY(expect); 87 | 88 | tmp = arr[i]; 89 | 90 | if (l1 === l2) { 91 | // no length offsets 92 | } else if (tmp.removed && arr[i + 1]) { 93 | let del = tmp.count - arr[i + 1].count; 94 | if (del == 0) { 95 | // wash~ 96 | } else if (del > 0) { 97 | expect = ' '.repeat(del) + expect; 98 | p2 = ' '.repeat(del) + p2; 99 | l2 += del; 100 | } else if (del < 0) { 101 | input = ' '.repeat(-del) + input; 102 | p1 = ' '.repeat(-del) + p1; 103 | l1 += -del; 104 | } 105 | } 106 | 107 | output += direct(p1, p2, l1, l2); 108 | 109 | if (l1 === l2) { 110 | for (tmp=' '; i < l1; i++) { 111 | tmp += input[i] === expect[i] ? ' ' : '^'; 112 | } 113 | } else { 114 | for (tmp=' '; i < arr.length; i++) { 115 | tmp += ((arr[i].added || arr[i].removed) ? '^' : ' ').repeat(Math.max(arr[i].count, 0)); 116 | if (i + 1 < arr.length && ((arr[i].added && arr[i+1].removed) || (arr[i].removed && arr[i+1].added))) { 117 | arr[i + 1].count -= arr[i].count; 118 | } 119 | } 120 | } 121 | 122 | return output + kleur.red(tmp); 123 | } 124 | 125 | export function direct(input, expect, lenA = String(input).length, lenB = String(expect).length) { 126 | let gutter = 4; 127 | let lenC = Math.max(lenA, lenB); 128 | let typeA=typeof input, typeB=typeof expect; 129 | 130 | if (typeA !== typeB) { 131 | gutter = 2; 132 | 133 | let delA = gutter + lenC - lenA; 134 | let delB = gutter + lenC - lenB; 135 | 136 | input += ' '.repeat(delA) + kleur.dim(`[${typeA}]`); 137 | expect += ' '.repeat(delB) + kleur.dim(`[${typeB}]`); 138 | 139 | lenA += delA + typeA.length + 2; 140 | lenB += delB + typeB.length + 2; 141 | lenC = Math.max(lenA, lenB); 142 | } 143 | 144 | let output = colors['++']('++' + expect + ' '.repeat(gutter + lenC - lenB) + TITLE('(Expected)')) + '\n'; 145 | return output + colors['--']('--' + input + ' '.repeat(gutter + lenC - lenA) + TITLE('(Actual)')) + '\n'; 146 | } 147 | 148 | export function sort(input, expect) { 149 | var k, i=0, tmp, isArr = Array.isArray(input); 150 | var keys=[], out=isArr ? Array(input.length) : {}; 151 | 152 | if (isArr) { 153 | for (i=0; i < out.length; i++) { 154 | tmp = input[i]; 155 | if (!tmp || typeof tmp !== 'object') out[i] = tmp; 156 | else out[i] = sort(tmp, expect[i]); // might not be right 157 | } 158 | } else { 159 | for (k in expect) 160 | keys.push(k); 161 | 162 | for (; i < keys.length; i++) { 163 | if (Object.prototype.hasOwnProperty.call(input, k = keys[i])) { 164 | if (!(tmp = input[k]) || typeof tmp !== 'object') out[k] = tmp; 165 | else out[k] = sort(tmp, expect[k]); 166 | } 167 | } 168 | 169 | for (k in input) { 170 | if (!out.hasOwnProperty(k)) { 171 | out[k] = input[k]; // expect didnt have 172 | } 173 | } 174 | } 175 | 176 | return out; 177 | } 178 | 179 | export function circular() { 180 | var cache = new Set; 181 | return function print(key, val) { 182 | if (val === void 0) return '[__VOID__]'; 183 | if (typeof val === 'number' && val !== val) return '[__NAN__]'; 184 | if (typeof val === 'bigint') return val.toString(); 185 | if (!val || typeof val !== 'object') return val; 186 | if (cache.has(val)) return '[Circular]'; 187 | cache.add(val); return val; 188 | } 189 | } 190 | 191 | export function stringify(input) { 192 | return JSON.stringify(input, circular(), 2).replace(/"\[__NAN__\]"/g, 'NaN').replace(/"\[__VOID__\]"/g, 'undefined'); 193 | } 194 | 195 | export function compare(input, expect) { 196 | if (Array.isArray(expect) && Array.isArray(input)) return arrays(input, expect); 197 | if (expect instanceof RegExp) return chars(''+input, ''+expect); 198 | 199 | let isA = input && typeof input == 'object'; 200 | let isB = expect && typeof expect == 'object'; 201 | 202 | if (isA && isB) input = sort(input, expect); 203 | if (isB) expect = stringify(expect); 204 | if (isA) input = stringify(input); 205 | 206 | if (expect && typeof expect == 'object') { 207 | input = stringify(sort(input, expect)); 208 | expect = stringify(expect); 209 | } 210 | 211 | isA = typeof input == 'string'; 212 | isB = typeof expect == 'string'; 213 | 214 | if (isA && /\r?\n/.test(input)) return lines(input, ''+expect); 215 | if (isB && /\r?\n/.test(expect)) return lines(''+input, expect); 216 | if (isA && isB) return chars(input, expect); 217 | 218 | return direct(input, expect); 219 | } 220 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace uvu { 2 | type Crumbs = { __suite__: string; __test__: string }; 3 | type Callback = (context: T & Crumbs) => Promise | void; 4 | 5 | interface Hook { 6 | (hook: Callback): void; 7 | each(hook: Callback): void; 8 | } 9 | 10 | interface Test { 11 | (name: string, test: Callback): void; 12 | only(name: string, test: Callback): void; 13 | skip(name?: string, test?: Callback): void; 14 | before: Hook; 15 | after: Hook 16 | run(): void; 17 | } 18 | } 19 | 20 | type Context = Record; 21 | 22 | export type Test = uvu.Test; 23 | export type Callback = uvu.Callback; 24 | 25 | export const test: uvu.Test; 26 | export function suite(title?: string, context?: T): uvu.Test; 27 | export function exec(bail?: boolean): Promise; 28 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import kleur from 'kleur'; 2 | import { compare } from 'uvu/diff'; 3 | 4 | let isCLI = false, isNode = false; 5 | let hrtime = (now = Date.now()) => () => (Date.now() - now).toFixed(2) + 'ms'; 6 | let write = console.log; 7 | 8 | const into = (ctx, key) => (name, handler) => ctx[key].push({ name, handler }); 9 | const context = (state) => ({ tests:[], before:[], after:[], bEach:[], aEach:[], only:[], skips:0, state }); 10 | const milli = arr => (arr[0]*1e3 + arr[1]/1e6).toFixed(2) + 'ms'; 11 | const hook = (ctx, key) => handler => ctx[key].push(handler); 12 | 13 | if (isNode = typeof process < 'u' && typeof process.stdout < 'u') { 14 | // globalThis polyfill; Node < 12 15 | if (typeof globalThis !== 'object') { 16 | Object.defineProperty(global, 'globalThis', { 17 | get: function () { return this } 18 | }); 19 | } 20 | 21 | let rgx = /(\.bin[\\+\/]uvu$|uvu[\\+\/]bin\.js)/i; 22 | isCLI = process.argv.some(x => rgx.test(x)); 23 | 24 | // attach node-specific utils 25 | write = x => process.stdout.write(x); 26 | hrtime = (now = process.hrtime()) => () => milli(process.hrtime(now)); 27 | } else if (typeof performance < 'u') { 28 | hrtime = (now = performance.now()) => () => (performance.now() - now).toFixed(2) + 'ms'; 29 | } 30 | 31 | globalThis.UVU_QUEUE = globalThis.UVU_QUEUE || []; 32 | isCLI = isCLI || !!globalThis.UVU_DEFER; 33 | isCLI || UVU_QUEUE.push([null]); 34 | 35 | const QUOTE = kleur.dim('"'), GUTTER = '\n '; 36 | const FAIL = kleur.red('✘ '), PASS = kleur.gray('• '); 37 | const IGNORE = /^\s*at.*(?:\(|\s)(?:node|(internal\/[\w/]*))/; 38 | const FAILURE = kleur.bold().bgRed(' FAIL '); 39 | const FILE = kleur.bold().underline().white; 40 | const SUITE = kleur.bgWhite().bold; 41 | 42 | function stack(stack, idx) { 43 | let i=0, line, out=''; 44 | let arr = stack.substring(idx).replace(/\\/g, '/').split('\n'); 45 | for (; i < arr.length; i++) { 46 | line = arr[i].trim(); 47 | if (line.length && !IGNORE.test(line)) { 48 | out += '\n ' + line; 49 | } 50 | } 51 | return kleur.grey(out) + '\n'; 52 | } 53 | 54 | function format(name, err, suite = '') { 55 | let { details, operator='' } = err; 56 | let idx = err.stack && err.stack.indexOf('\n'); 57 | if (err.name.startsWith('AssertionError') && !operator.includes('not')) details = compare(err.actual, err.expected); // TODO? 58 | let str = ' ' + FAILURE + (suite ? kleur.red(SUITE(` ${suite} `)) : '') + ' ' + QUOTE + kleur.red().bold(name) + QUOTE; 59 | str += '\n ' + err.message + (operator ? kleur.italic().dim(` (${operator})`) : '') + '\n'; 60 | if (details) str += GUTTER + details.split('\n').join(GUTTER); 61 | if (!!~idx) str += stack(err.stack, idx); 62 | return str + '\n'; 63 | } 64 | 65 | async function runner(ctx, name) { 66 | let { only, tests, before, after, bEach, aEach, state } = ctx; 67 | let hook, test, arr = only.length ? only : tests; 68 | let num=0, errors='', total=arr.length; 69 | 70 | try { 71 | if (name) write(SUITE(kleur.black(` ${name} `)) + ' '); 72 | for (hook of before) await hook(state); 73 | 74 | for (test of arr) { 75 | state.__test__ = test.name; 76 | try { 77 | for (hook of bEach) await hook(state); 78 | await test.handler(state); 79 | for (hook of aEach) await hook(state); 80 | write(PASS); 81 | num++; 82 | } catch (err) { 83 | for (hook of aEach) await hook(state); 84 | if (errors.length) errors += '\n'; 85 | errors += format(test.name, err, name); 86 | write(FAIL); 87 | } 88 | } 89 | } finally { 90 | state.__test__ = ''; 91 | for (hook of after) await hook(state); 92 | let msg = ` (${num} / ${total})\n`; 93 | let skipped = (only.length ? tests.length : 0) + ctx.skips; 94 | write(errors.length ? kleur.red(msg) : kleur.green(msg)); 95 | return [errors || true, num, skipped, total]; 96 | } 97 | } 98 | 99 | let timer; 100 | function defer() { 101 | clearTimeout(timer); 102 | timer = setTimeout(exec); 103 | } 104 | 105 | function setup(ctx, name = '') { 106 | ctx.state.__test__ = ''; 107 | ctx.state.__suite__ = name; 108 | const test = into(ctx, 'tests'); 109 | test.before = hook(ctx, 'before'); 110 | test.before.each = hook(ctx, 'bEach'); 111 | test.after = hook(ctx, 'after'); 112 | test.after.each = hook(ctx, 'aEach'); 113 | test.only = into(ctx, 'only'); 114 | test.skip = () => { ctx.skips++ }; 115 | test.run = () => { 116 | let copy = { ...ctx }; 117 | let run = runner.bind(0, copy, name); 118 | Object.assign(ctx, context(copy.state)); 119 | UVU_QUEUE[globalThis.UVU_INDEX || 0].push(run); 120 | isCLI || defer(); 121 | }; 122 | return test; 123 | } 124 | 125 | export const suite = (name = '', state = {}) => setup(context(state), name); 126 | export const test = suite(); 127 | 128 | let isRunning = false; 129 | export async function exec(bail) { 130 | let timer = hrtime(); 131 | let done=0, total=0, skips=0, code=0; 132 | 133 | isRunning = true; 134 | for (let group of UVU_QUEUE) { 135 | if (total) write('\n'); 136 | 137 | let name = group.shift(); 138 | if (name != null) write(FILE(name) + '\n'); 139 | 140 | for (let test of group) { 141 | let [errs, ran, skip, max] = await test(); 142 | total += max; done += ran; skips += skip; 143 | if (errs.length) { 144 | write('\n' + errs + '\n'); code=1; 145 | if (bail) return isNode && process.exit(1); 146 | } 147 | } 148 | } 149 | 150 | isRunning = false; 151 | write('\n Total: ' + total); 152 | write((code ? kleur.red : kleur.green)('\n Passed: ' + done)); 153 | write('\n Skipped: ' + (skips ? kleur.yellow(skips) : skips)); 154 | write('\n Duration: ' + timer() + '\n\n'); 155 | 156 | if (isNode) process.exitCode = code; 157 | } 158 | 159 | if (isNode) process.on('exit', () => { 160 | if (!isRunning) return; // okay to exit 161 | process.exitCode = process.exitCode || 1; 162 | console.error('Exiting early before testing is finished.'); 163 | }); 164 | -------------------------------------------------------------------------------- /test/assert.js: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as $ from '../src/assert'; 4 | 5 | function isError(err, msg, input, expect, operator, details) { 6 | assert.instance(err, Error); 7 | if (msg) assert.is(err.message, msg, '~> message'); 8 | assert.is(!!err.generated, !msg, '~> generated'); 9 | assert.ok(err.stack, '~> stack'); 10 | 11 | assert.instance(err, $.Assertion); 12 | assert.is(err.name, 'Assertion'); 13 | assert.is(err.code, 'ERR_ASSERTION'); 14 | assert.is(!!err.details, !!details, '~> details'); 15 | assert.is(err.operator, operator, '~> operator'); 16 | assert.equal(err.expects, expect, '~> expects'); 17 | assert.equal(err.actual, input, '~> actual'); 18 | } 19 | 20 | const Assertion = suite('Assertion'); 21 | 22 | Assertion('should extend Error', () => { 23 | assert.instance(new $.Assertion(), Error); 24 | }); 25 | 26 | Assertion('should expect input options', () => { 27 | let err = new $.Assertion({ 28 | actual: 'foo', 29 | operator: 'is', 30 | expects: 'bar', 31 | message: 'Expected "foo" to equal "bar"', 32 | generated: false, 33 | details: true, 34 | }); 35 | isError( 36 | err, 37 | 'Expected "foo" to equal "bar"', 38 | 'foo', 'bar', 'is', true 39 | ); 40 | }); 41 | 42 | Assertion.run(); 43 | 44 | // --- 45 | 46 | const ok = suite('ok'); 47 | 48 | ok('should be a function', () => { 49 | assert.type($.ok, 'function'); 50 | }); 51 | 52 | ok('should not throw if valid', () => { 53 | assert.not.throws(() => $.ok(true)); 54 | }); 55 | 56 | ok('should throw if invalid', () => { 57 | try { 58 | $.ok(false); 59 | } catch (err) { 60 | isError(err, '', false, true, 'ok', false); 61 | } 62 | }); 63 | 64 | ok('should throw with custom message', () => { 65 | try { 66 | $.ok(false, 'hello there'); 67 | } catch (err) { 68 | isError(err, 'hello there', false, true, 'ok', false); 69 | } 70 | }); 71 | 72 | ok.run(); 73 | 74 | // --- 75 | 76 | const is = suite('is'); 77 | 78 | is('should be a function', () => { 79 | assert.type($.is, 'function'); 80 | }); 81 | 82 | is('should not throw if valid', () => { 83 | assert.not.throws(() => $.is('abc', 'abc')); 84 | assert.not.throws(() => $.is(true, true)); 85 | assert.not.throws(() => $.is(123, 123)); 86 | }); 87 | 88 | is('should throw if invalid', () => { 89 | try { 90 | $.is('foo', 'bar'); 91 | } catch (err) { 92 | isError(err, '', 'foo', 'bar', 'is', true); 93 | } 94 | }); 95 | 96 | is('should throw with custom message', () => { 97 | try { 98 | $.is(123, 456, 'hello there'); 99 | } catch (err) { 100 | isError(err, 'hello there', 123, 456, 'is', true); 101 | } 102 | }); 103 | 104 | // TODO: add throws() instance matcher? 105 | // assert.throws(() => {}, $.Assertion) 106 | is('should use strict equality checks', () => { 107 | try { 108 | let arr = [3, 4, 5]; 109 | $.is(arr, arr.slice()); 110 | assert.unreachable(); 111 | } catch (err) { 112 | assert.instance(err, $.Assertion); 113 | } 114 | 115 | try { 116 | let obj = { foo: 123 } 117 | $.is(obj, { ...obj }); 118 | assert.unreachable(); 119 | } catch (err) { 120 | assert.instance(err, $.Assertion); 121 | } 122 | }); 123 | 124 | is.run(); 125 | 126 | // --- 127 | 128 | const equal = suite('equal'); 129 | 130 | equal('should be a function', () => { 131 | assert.type($.equal, 'function'); 132 | }); 133 | 134 | equal('should not throw if valid', () => { 135 | assert.not.throws(() => $.equal('abc', 'abc')); 136 | assert.not.throws(() => $.equal(true, true)); 137 | assert.not.throws(() => $.equal(123, 123)); 138 | 139 | assert.not.throws(() => $.equal([1, 2, 3], [1, 2, 3])); 140 | assert.not.throws(() => $.equal({ foo: [2, 3] }, { foo: [2, 3] })); 141 | }); 142 | 143 | equal('should throw if invalid', () => { 144 | let input = { 145 | foo: ['aaa'], 146 | bar: [2, 3] 147 | }; 148 | 149 | let expect = { 150 | foo: ['a', 'a'], 151 | bar: [{ a: 1, b: 2 }] 152 | }; 153 | 154 | try { 155 | $.equal(input, expect); 156 | } catch (err) { 157 | isError(err, '', input, expect, 'equal', true); 158 | } 159 | }); 160 | 161 | equal('should throw with custom message', () => { 162 | let input = { 163 | foo: ['aaa'], 164 | bar: [2, 3] 165 | }; 166 | 167 | let expect = { 168 | foo: ['a', 'a'], 169 | bar: [{ a: 1, b: 2 }] 170 | }; 171 | 172 | try { 173 | $.equal(input, expect, 'hello there'); 174 | } catch (err) { 175 | isError(err, 'hello there', input, expect, 'equal', true); 176 | } 177 | }); 178 | 179 | equal('should use deep equality checks', () => { 180 | try { 181 | $.equal( 182 | { a:[{ b:2 }] }, 183 | { a:[{ b:1 }] } 184 | ); 185 | assert.unreachable(); 186 | } catch (err) { 187 | assert.instance(err, $.Assertion); 188 | } 189 | 190 | try { 191 | $.equal( 192 | [{ a:2 }, { b:2 }], 193 | [{ a:1 }, { b:2 }] 194 | ); 195 | assert.unreachable(); 196 | } catch (err) { 197 | assert.instance(err, $.Assertion); 198 | } 199 | }); 200 | 201 | equal('should throw assertion error on array type mismatch', () => { 202 | try { 203 | $.equal(null, []); 204 | assert.unreachable(); 205 | } catch (err) { 206 | assert.instance(err, $.Assertion); 207 | } 208 | 209 | try { 210 | $.equal([], null); 211 | assert.unreachable(); 212 | } catch (err) { 213 | assert.instance(err, $.Assertion); 214 | } 215 | 216 | }); 217 | 218 | equal.run(); 219 | 220 | // --- 221 | 222 | const unreachable = suite('unreachable'); 223 | 224 | unreachable('should be a function', () => { 225 | assert.type($.unreachable, 'function'); 226 | }); 227 | 228 | unreachable('should always throw', () => { 229 | try { 230 | $.unreachable(); 231 | } catch (err) { 232 | isError(err, '', true, false, 'unreachable', false); 233 | } 234 | }); 235 | 236 | unreachable('should customize message', () => { 237 | try { 238 | $.unreachable('hello'); 239 | } catch (err) { 240 | isError(err, 'hello', true, false, 'unreachable', false); 241 | } 242 | }); 243 | 244 | unreachable.run(); 245 | 246 | // --- 247 | 248 | const instance = suite('instance'); 249 | 250 | instance('should be a function', () => { 251 | assert.type($.instance, 'function'); 252 | }); 253 | 254 | instance('should not throw if valid', () => { 255 | assert.not.throws(() => $.instance(new Date, Date)); 256 | assert.not.throws(() => $.instance(/foo/, RegExp)); 257 | assert.not.throws(() => $.instance({}, Object)); 258 | assert.not.throws(() => $.instance([], Array)); 259 | }); 260 | 261 | instance('should throw if invalid', () => { 262 | try { 263 | $.instance('foo', Error); 264 | } catch (err) { 265 | isError(err, '', 'foo', Error, 'instance', false); 266 | } 267 | }); 268 | 269 | instance('should throw with custom message', () => { 270 | try { 271 | $.instance('foo', Error, 'hello there'); 272 | } catch (err) { 273 | isError(err, 'hello there', 'foo', Error, 'instance', false); 274 | } 275 | }); 276 | 277 | instance.run(); 278 | 279 | // --- 280 | 281 | const type = suite('type'); 282 | 283 | type('should be a function', () => { 284 | assert.type($.type, 'function'); 285 | }); 286 | 287 | type('should not throw if valid', () => { 288 | assert.not.throws(() => $.type(123, 'number')); 289 | assert.not.throws(() => $.type(true, 'boolean')); 290 | assert.not.throws(() => $.type($.type, 'function')); 291 | assert.not.throws(() => $.type('abc', 'string')); 292 | assert.not.throws(() => $.type(/x/, 'object')); 293 | }); 294 | 295 | type('should throw if invalid', () => { 296 | try { 297 | $.type('foo', 'number'); 298 | } catch (err) { 299 | isError(err, '', 'string', 'number', 'type', false); 300 | } 301 | }); 302 | 303 | type('should throw with custom message', () => { 304 | try { 305 | $.type('foo', 'number', 'hello there'); 306 | } catch (err) { 307 | isError(err, 'hello there', 'string', 'number', 'type', false); 308 | } 309 | }); 310 | 311 | type.run(); 312 | 313 | // --- 314 | 315 | // TODO 316 | const snapshot = suite('snapshot'); 317 | 318 | snapshot('should be a function', () => { 319 | assert.type($.snapshot, 'function'); 320 | }); 321 | 322 | snapshot.run(); 323 | 324 | // --- 325 | 326 | // TODO 327 | const fixture = suite('fixture'); 328 | 329 | fixture('should be a function', () => { 330 | assert.type($.fixture, 'function'); 331 | }); 332 | 333 | fixture.run(); 334 | 335 | // --- 336 | 337 | const match = suite('match'); 338 | 339 | match('should be a function', () => { 340 | assert.type($.match, 'function'); 341 | }); 342 | 343 | match('should not throw if valid', () => { 344 | assert.not.throws(() => $.match('foobar', 'foo')); 345 | assert.not.throws(() => $.match('foobar', 'bar')); 346 | 347 | assert.not.throws(() => $.match('foobar', /foo/)); 348 | assert.not.throws(() => $.match('foobar', /bar/i)); 349 | }); 350 | 351 | match('should throw if invalid', () => { 352 | try { 353 | $.match('foobar', 'hello'); 354 | } catch (err) { 355 | isError(err, '', 'foobar', 'hello', 'match', false); 356 | } 357 | 358 | try { 359 | $.match('foobar', /hello/i); 360 | } catch (err) { 361 | isError(err, '', 'foobar', /hello/i, 'match', false); 362 | } 363 | }); 364 | 365 | match('should throw with custom message', () => { 366 | try { 367 | $.match('foobar', 'hello', 'howdy partner'); 368 | } catch (err) { 369 | isError(err, 'howdy partner', 'foobar', 'hello', 'match', false); 370 | } 371 | }); 372 | 373 | match.run(); 374 | 375 | // --- 376 | 377 | const throws = suite('throws'); 378 | 379 | throws('should be a function', () => { 380 | assert.type($.throws, 'function'); 381 | }); 382 | 383 | throws('should throw if function does not throw Error :: generic', () => { 384 | try { 385 | $.throws(() => 123); 386 | } catch (err) { 387 | assert.is(err.message, 'Expected function to throw'); 388 | isError(err, '', false, true, 'throws', false); // no details (true vs false) 389 | } 390 | }); 391 | 392 | throws('should throw if function does not throw matching Error :: RegExp', () => { 393 | try { 394 | $.throws(() => { throw new Error('hello') }, /world/); 395 | } catch (err) { 396 | assert.is(err.message, 'Expected function to throw exception matching `/world/` pattern'); 397 | isError(err, '', false, true, 'throws', false); // no details 398 | } 399 | }); 400 | 401 | throws('should throw if function does not throw matching Error :: Function', () => { 402 | try { 403 | $.throws( 404 | () => { throw new Error }, 405 | (err) => err.message.includes('foobar') 406 | ); 407 | } catch (err) { 408 | assert.is(err.message, 'Expected function to throw matching exception'); 409 | isError(err, '', false, true, 'throws', false); // no details 410 | } 411 | }); 412 | 413 | throws('should not throw if function does throw Error :: generic', () => { 414 | assert.not.throws( 415 | () => $.throws(() => { throw new Error }) 416 | ); 417 | }); 418 | 419 | throws('should not throw if function does throw matching Error :: RegExp', () => { 420 | assert.not.throws( 421 | () => $.throws(() => { throw new Error('hello') }, /hello/) 422 | ); 423 | }); 424 | 425 | throws('should not throw if function does throw matching Error :: Function', () => { 426 | assert.not.throws(() => { 427 | $.throws( 428 | () => { throw new Error('foobar') }, 429 | (err) => err.message.includes('foobar') 430 | ) 431 | }); 432 | }); 433 | 434 | throws.run(); 435 | 436 | // --- 437 | 438 | const not = suite('not'); 439 | 440 | not('should be a function', () => { 441 | assert.type($.not, 'function'); 442 | }); 443 | 444 | not('should not throw if falsey', () => { 445 | assert.not.throws(() => $.not(false)); 446 | assert.not.throws(() => $.not(undefined)); 447 | assert.not.throws(() => $.not(null)); 448 | assert.not.throws(() => $.not('')); 449 | assert.not.throws(() => $.not(0)); 450 | }); 451 | 452 | not('should throw if truthy', () => { 453 | try { 454 | $.not(true); 455 | } catch (err) { 456 | isError(err, '', true, false, 'not', false); 457 | } 458 | }); 459 | 460 | not('should throw with custom message', () => { 461 | try { 462 | $.not(true, 'hello there'); 463 | } catch (err) { 464 | isError(err, 'hello there', true, false, 'not', false); 465 | } 466 | }); 467 | 468 | not.run(); 469 | 470 | // --- 471 | 472 | const notOk = suite('not.ok'); 473 | 474 | notOk('should be a function', () => { 475 | assert.type($.not.ok, 'function'); 476 | }); 477 | 478 | notOk('should not throw if falsey', () => { 479 | assert.not.throws(() => $.not.ok(false)); 480 | assert.not.throws(() => $.not.ok(undefined)); 481 | assert.not.throws(() => $.not.ok(null)); 482 | assert.not.throws(() => $.not.ok('')); 483 | assert.not.throws(() => $.not.ok(0)); 484 | }); 485 | 486 | notOk('should throw if truthy', () => { 487 | try { 488 | $.not.ok(true); 489 | } catch (err) { 490 | isError(err, '', true, false, 'not', false); 491 | } 492 | }); 493 | 494 | notOk('should throw with custom message', () => { 495 | try { 496 | $.not.ok(true, 'hello there'); 497 | } catch (err) { 498 | isError(err, 'hello there', true, false, 'not', false); 499 | } 500 | }); 501 | 502 | notOk.run(); 503 | 504 | // --- 505 | 506 | const notIs = suite('is.not'); 507 | 508 | notIs('should be a function', () => { 509 | assert.type($.is.not, 'function'); 510 | }); 511 | 512 | notIs('should not throw if values are not strictly equal', () => { 513 | assert.not.throws(() => $.is.not(true, false)); 514 | assert.not.throws(() => $.is.not(123, undefined)); 515 | assert.not.throws(() => $.is.not('123', 123)); 516 | assert.not.throws(() => $.is.not(NaN, NaN)); 517 | assert.not.throws(() => $.is.not([], [])); 518 | }); 519 | 520 | notIs('should throw if values are strictly equal', () => { 521 | try { 522 | $.is.not('hello', 'hello'); 523 | } catch (err) { 524 | isError(err, '', 'hello', 'hello', 'is.not', false); 525 | } 526 | }); 527 | 528 | notIs('should throw with custom message', () => { 529 | try { 530 | $.is.not('foo', 'foo', 'hello there'); 531 | } catch (err) { 532 | isError(err, 'hello there', 'foo', 'foo', 'is.not', false); 533 | } 534 | }); 535 | 536 | notIs.run(); 537 | 538 | // --- 539 | 540 | const notEqual = suite('not.equal'); 541 | 542 | notEqual('should be a function', () => { 543 | assert.type($.not.equal, 'function'); 544 | }); 545 | 546 | notEqual('should throw if values are deeply equal', () => { 547 | try { 548 | $.not.equal('abc', 'abc'); 549 | } catch (err) { 550 | isError(err, '', 'abc', 'abc', 'not.equal', false); // no details 551 | } 552 | 553 | try { 554 | $.not.equal(true, true); 555 | } catch (err) { 556 | isError(err, '', true, true, 'not.equal', false); // no details 557 | } 558 | 559 | try { 560 | $.not.equal(123, 123); 561 | } catch (err) { 562 | isError(err, '', 123, 123, 'not.equal', false); // no details 563 | } 564 | 565 | let arr = [1, 2, 3]; 566 | let obj = { foo: [2, 3] }; 567 | 568 | try { 569 | $.not.equal(arr, arr); 570 | } catch (err) { 571 | isError(err, '', arr, arr, 'not.equal', false); // no details 572 | } 573 | 574 | try { 575 | $.not.equal(obj, obj); 576 | } catch (err) { 577 | isError(err, '', obj, obj, 'not.equal', false); // no details 578 | } 579 | }); 580 | 581 | notEqual('should not throw if values are not deeply equal', () => { 582 | let input = { 583 | foo: ['aaa'], 584 | bar: [2, 3] 585 | }; 586 | 587 | let expect = { 588 | foo: ['a', 'a'], 589 | bar: [{ a: 1, b: 2 }] 590 | }; 591 | 592 | assert.not.throws( 593 | () => $.not.equal(input, expect) 594 | ); 595 | }); 596 | 597 | notEqual('should throw with custom message', () => { 598 | let input = { 599 | foo: ['aaa'], 600 | bar: [2, 3] 601 | }; 602 | 603 | try { 604 | $.not.equal(input, input, 'hello there'); 605 | } catch (err) { 606 | isError(err, 'hello there', input, input, 'not.equal', false); // no details 607 | } 608 | }); 609 | 610 | notEqual('should use deep equality checks', () => { 611 | try { 612 | $.not.equal( 613 | { a:[{ b:2 }] }, 614 | { a:[{ b:2 }] }, 615 | ); 616 | assert.unreachable(); 617 | } catch (err) { 618 | assert.instance(err, $.Assertion); 619 | assert.is(err.operator, 'not.equal'); 620 | } 621 | 622 | try { 623 | $.not.equal( 624 | [{ a:2 }, { b:2 }], 625 | [{ a:2 }, { b:2 }], 626 | ); 627 | assert.unreachable(); 628 | } catch (err) { 629 | assert.instance(err, $.Assertion); 630 | assert.is(err.operator, 'not.equal'); 631 | } 632 | }); 633 | 634 | notEqual.run(); 635 | 636 | // --- 637 | 638 | const notType = suite('not.type'); 639 | 640 | notType('should be a function', () => { 641 | assert.type($.not.type, 'function'); 642 | }); 643 | 644 | notType('should throw if types match', () => { 645 | try { 646 | $.not.type(123, 'number'); 647 | } catch (err) { 648 | isError(err, '', 'number', 'number', 'not.type', false); 649 | } 650 | 651 | try { 652 | $.not.type(true, 'boolean'); 653 | } catch (err) { 654 | isError(err, '', 'boolean', 'boolean', 'not.type', false); 655 | } 656 | 657 | try { 658 | $.not.type($.not.type, 'function'); 659 | } catch (err) { 660 | isError(err, '', 'function', 'function', 'not.type', false); 661 | } 662 | 663 | try { 664 | $.not.type('abc', 'string'); 665 | } catch (err) { 666 | isError(err, '', 'string', 'string', 'not.type', false); 667 | } 668 | 669 | try { 670 | $.not.type(/x/, 'object'); 671 | } catch (err) { 672 | isError(err, '', 'object', 'object', 'not.type', false); 673 | } 674 | }); 675 | 676 | notType('should not throw if types do not match', () => { 677 | assert.not.throws( 678 | () => $.not.type('foo', 'number') 679 | ); 680 | }); 681 | 682 | notType('should throw with custom message', () => { 683 | try { 684 | $.not.type('abc', 'string', 'hello world'); 685 | } catch (err) { 686 | isError(err, 'hello world', 'string', 'string', 'not.type', false); 687 | } 688 | }); 689 | 690 | notType.run(); 691 | 692 | // --- 693 | 694 | const notInstance = suite('not.instance'); 695 | 696 | notInstance('should be a function', () => { 697 | assert.type($.not.instance, 'function'); 698 | }); 699 | 700 | notInstance('should throw if values match', () => { 701 | // isError uses is() check -- lazy 702 | let inputs = { 703 | date: new Date, 704 | regexp: /foo/, 705 | object: {}, 706 | array: [], 707 | }; 708 | 709 | try { 710 | $.not.instance(inputs.date, Date); 711 | } catch (err) { 712 | isError(err, '', inputs.date, Date, 'not.instance', false); 713 | } 714 | 715 | try { 716 | $.not.instance(inputs.regexp, RegExp); 717 | } catch (err) { 718 | isError(err, '', inputs.regexp, RegExp, 'not.instance', false); 719 | } 720 | 721 | try { 722 | $.not.instance(inputs.object, Object); 723 | } catch (err) { 724 | isError(err, '', inputs.object, Object, 'not.instance', false); 725 | } 726 | 727 | try { 728 | $.not.instance(inputs.array, Array); 729 | } catch (err) { 730 | isError(err, '', inputs.array, Array, 'not.instance', false); 731 | } 732 | }); 733 | 734 | notInstance('should not throw on mismatch', () => { 735 | assert.not.throws(() => $.not.instance('foo', Error)); 736 | }); 737 | 738 | notInstance('should throw with custom message', () => { 739 | try { 740 | $.not.instance('foo', String, 'hello there'); 741 | } catch (err) { 742 | isError(err, 'hello there', 'foo', String, 'instance', false); 743 | } 744 | }); 745 | 746 | notInstance.run(); 747 | 748 | // --- 749 | 750 | // TODO 751 | // const notSnapshot = suite('not.snapshot'); 752 | // notSnapshot.run(); 753 | 754 | // --- 755 | 756 | // TODO 757 | // const notFixture = suite('not.fixture'); 758 | // notFixture.run(); 759 | 760 | // --- 761 | 762 | const notMatch = suite('not.match'); 763 | 764 | notMatch('should be a function', () => { 765 | assert.type($.not.match, 'function'); 766 | }); 767 | 768 | notMatch('should throw if values match', () => { 769 | try { 770 | $.not.match('foobar', 'foo'); 771 | } catch (err) { 772 | isError(err, '', 'foobar', 'foo', 'not.match', false); 773 | } 774 | 775 | try { 776 | $.not.match('foobar', 'bar'); 777 | } catch (err) { 778 | isError(err, '', 'foobar', 'bar', 'not.match', false); 779 | } 780 | 781 | try { 782 | $.not.match('foobar', /foo/); 783 | } catch (err) { 784 | isError(err, '', 'foobar', /foo/, 'not.match', false); 785 | } 786 | }); 787 | 788 | notMatch('should not throw if types do not match', () => { 789 | assert.not.throws( 790 | () => $.not.match('foobar', 'hello') 791 | ); 792 | 793 | assert.not.throws( 794 | () => $.not.match('foobar', /hello/) 795 | ); 796 | }); 797 | 798 | notMatch('should throw with custom message', () => { 799 | try { 800 | $.not.match('foobar', 'hello', 'hello world'); 801 | } catch (err) { 802 | isError(err, 'hello world', 'foobar', 'hello', 'not.match', false); 803 | } 804 | }); 805 | 806 | notMatch.run(); 807 | 808 | // --- 809 | 810 | const notThrows = suite('not.throws'); 811 | 812 | notThrows('should be a function', () => { 813 | assert.type($.throws, 'function'); 814 | }); 815 | 816 | notThrows('should not throw if function does not throw Error :: generic', () => { 817 | assert.not.throws( 818 | () => $.not.throws(() => 123) 819 | ); 820 | }); 821 | 822 | notThrows('should not throw if function does not throw matching Error :: RegExp', () => { 823 | assert.not.throws( 824 | () => $.not.throws(() => { throw new Error('hello') }, /world/) 825 | ); 826 | }); 827 | 828 | notThrows('should not throw if function does not throw matching Error :: Function', () => { 829 | assert.not.throws(() => { 830 | $.not.throws( 831 | () => { throw new Error('hello') }, 832 | (err) => err.message.includes('world') 833 | ) 834 | }); 835 | }); 836 | 837 | notThrows('should throw if function does throw Error :: generic', () => { 838 | try { 839 | $.not.throws(() => { throw new Error }); 840 | } catch (err) { 841 | assert.is(err.message, 'Expected function not to throw'); 842 | isError(err, '', true, false, 'not.throws', false); // no details 843 | } 844 | }); 845 | 846 | notThrows('should throw if function does throw matching Error :: RegExp', () => { 847 | try { 848 | $.not.throws(() => { throw new Error('hello') }, /hello/); 849 | } catch (err) { 850 | assert.is(err.message, 'Expected function not to throw exception matching `/hello/` pattern'); 851 | isError(err, '', true, false, 'not.throws', false); // no details 852 | } 853 | }); 854 | 855 | notThrows('should throw if function does throw matching Error :: Function', () => { 856 | try { 857 | $.not.throws( 858 | () => { throw new Error }, 859 | (err) => err instanceof Error 860 | ); 861 | } catch (err) { 862 | assert.is(err.message, 'Expected function not to throw matching exception'); 863 | isError(err, '', true, false, 'not.throws', false); // no details 864 | } 865 | }); 866 | 867 | notThrows.run(); 868 | -------------------------------------------------------------------------------- /test/diff.js: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as $ from '../src/diff'; 4 | 5 | const isNode8 = process.versions.node.startsWith('8.'); 6 | const strip = str => str.replace(/[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[-a-zA-Z\d\/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-ntqry=><~]))/g, ''); 7 | 8 | const arrays = suite('arrays'); 9 | 10 | arrays('should be a function', () => { 11 | assert.type($.arrays, 'function'); 12 | }); 13 | 14 | arrays('should handle simple values', () => { 15 | assert.is( 16 | strip($.arrays([1, 2, 3], [1, 2, 4])), 17 | '··[\n' + 18 | '····1,\n' + 19 | '····2,\n' + 20 | 'Actual:\n' + 21 | '--··3,\n' + 22 | 'Expected:\n' + 23 | '++··4,\n' + 24 | '··]\n' 25 | ); 26 | }); 27 | 28 | arrays('should handle nullish values', () => { 29 | assert.is( 30 | strip($.arrays(['foo', 'bar', undefined], ['foo', 'bar', 'baz'])), 31 | '··[\n' + 32 | '····"foo",\n' + 33 | '····"bar",\n' + 34 | 'Actual:\n' + 35 | '--··undefined,\n' + 36 | 'Expected:\n' + 37 | '++··"baz",\n' + 38 | '··]\n' 39 | ); 40 | 41 | assert.is( 42 | strip($.arrays([1, 2, NaN, undefined], [1, 2, null, null])), 43 | '··[\n' + 44 | '····1,\n' + 45 | '····2,\n' + 46 | 'Actual:\n' + 47 | '--··NaN,\n' + 48 | '--··undefined,\n' + 49 | 'Expected:\n' + 50 | '++··null,\n' + 51 | '++··null,\n' + 52 | '··]\n' 53 | ); 54 | }); 55 | 56 | arrays('should allow dangling "Actual" block', () => { 57 | assert.is( 58 | strip($.arrays([1, 2, 3, 4], [1, 2, 4])), 59 | '··[\n' + 60 | '····1,\n' + 61 | '····2,\n' + 62 | 'Actual:\n' + 63 | '--··3,\n' + 64 | '····4,\n' + 65 | '··]\n' 66 | ); 67 | }); 68 | 69 | arrays('should allow dangling "Expected" block', () => { 70 | assert.is( 71 | strip($.arrays([1, 2, 4], [1, 2, 3, 4])), 72 | '··[\n' + 73 | '····1,\n' + 74 | '····2,\n' + 75 | 'Expected:\n' + 76 | '++··3,\n' + 77 | '····4,\n' + 78 | '··]\n' 79 | ); 80 | }); 81 | 82 | // TODO: improve later 83 | arrays('should print/bail on complex objects', () => { 84 | assert.is( 85 | strip( 86 | $.arrays( 87 | [{ foo: 1 }, { bar: 2 }], 88 | [{ foo: 1 }] 89 | ) 90 | ), 91 | '··[\n' + 92 | 'Actual:\n' + 93 | '--··{\n' + 94 | '--····"foo":·1\n' + 95 | '--··},\n' + 96 | '--··{\n' + 97 | '--····"bar":·2\n' + 98 | '--··}\n' + 99 | 'Expected:\n' + 100 | '++··{\n' + 101 | '++····"foo":·1\n' + 102 | '++··}\n' + 103 | '··]\n' 104 | ); 105 | 106 | assert.is( 107 | strip( 108 | $.arrays( 109 | [ [111], ['bbb'] ], 110 | [ [333], ['xxx'] ], 111 | ) 112 | ), 113 | '··[\n' + 114 | 'Actual:\n' + 115 | '--··[\n' + 116 | '--····111\n' + 117 | '--··],\n' + 118 | '--··[\n' + 119 | '--····"bbb"\n' + 120 | '--··]\n' + 121 | 'Expected:\n' + 122 | '++··[\n' + 123 | '++····333\n' + 124 | '++··],\n' + 125 | '++··[\n' + 126 | '++····"xxx"\n' + 127 | '++··]\n' + 128 | '··]\n' 129 | ); 130 | 131 | assert.is( 132 | strip( 133 | $.arrays( 134 | [ [111, 222], ['aaa', 'bbb'] ], 135 | [ [333, 444], ['aaa', 'xxx'] ], 136 | ) 137 | ), 138 | '··[\n' + 139 | 'Actual:\n' + 140 | '--··[\n' + 141 | '--····111,\n' + 142 | '--····222\n' + 143 | '--··],\n' + 144 | '--··[\n' + 145 | '--····"aaa",\n' + 146 | '--····"bbb"\n' + 147 | '--··]\n' + 148 | 'Expected:\n' + 149 | '++··[\n' + 150 | '++····333,\n' + 151 | '++····444\n' + 152 | '++··],\n' + 153 | '++··[\n' + 154 | '++····"aaa",\n' + 155 | '++····"xxx"\n' + 156 | '++··]\n' + 157 | '··]\n' 158 | ); 159 | 160 | assert.is( 161 | strip( 162 | $.arrays( 163 | [{ 164 | foobar: 123, 165 | position: { 166 | start: { line: 1, column: 1, offset: 0, index: 0 }, 167 | end: { line: 1, column: 8, offset: 7 } 168 | } 169 | }], 170 | [{ 171 | foobar: 456, 172 | position: { 173 | start: { line: 2, column: 1, offset: 0, index: 0 }, 174 | end: { line: 9, column: 9, offset: 6 } 175 | } 176 | }] 177 | ) 178 | ), 179 | '··[\n' + 180 | 'Actual:\n' + 181 | '--··{\n' + 182 | '--····"foobar":·123,\n' + 183 | '--····"position":·{\n' + 184 | '--······"start":·{\n' + 185 | '--········"line":·1,\n' + 186 | '--········"column":·1,\n' + 187 | '--········"offset":·0,\n' + 188 | '--········"index":·0\n' + 189 | '--······},\n' + 190 | '--······"end":·{\n' + 191 | '--········"line":·1,\n' + 192 | '--········"column":·8,\n' + 193 | '--········"offset":·7\n' + 194 | '--······}\n' + 195 | '--····}\n' + 196 | '--··}\n' + 197 | 'Expected:\n' + 198 | '++··{\n' + 199 | '++····"foobar":·456,\n' + 200 | '++····"position":·{\n' + 201 | '++······"start":·{\n' + 202 | '++········"line":·2,\n' + 203 | '++········"column":·1,\n' + 204 | '++········"offset":·0,\n' + 205 | '++········"index":·0\n' + 206 | '++······},\n' + 207 | '++······"end":·{\n' + 208 | '++········"line":·9,\n' + 209 | '++········"column":·9,\n' + 210 | '++········"offset":·6\n' + 211 | '++······}\n' + 212 | '++····}\n' + 213 | '++··}\n' + 214 | '··]\n' 215 | ); 216 | }); 217 | 218 | arrays.run(); 219 | 220 | // --- 221 | 222 | const lines = suite('lines'); 223 | 224 | lines('should be a function', () => { 225 | assert.type($.lines, 'function'); 226 | }); 227 | 228 | lines('should split on `\\r\\n` chars', () => { 229 | assert.is( 230 | strip($.lines('foo\nbar', 'foo\nbat')), 231 | '··foo\n' + 232 | 'Actual:\n' + 233 | '--bar\n' + 234 | 'Expected:\n' + 235 | '++bat\n' 236 | ); 237 | 238 | assert.is( 239 | strip($.lines('foo\r\nbar', 'foo\r\nbat')), 240 | '··foo\n' + 241 | 'Actual:\n' + 242 | '--bar\n' + 243 | 'Expected:\n' + 244 | '++bat\n' 245 | ); 246 | }); 247 | 248 | lines('should allow for dangling "Actual" block', () => { 249 | assert.is( 250 | strip($.lines('foo\nbar\nbaz', 'foo\nbaz')), 251 | '··foo\n' + 252 | 'Actual:\n' + 253 | '--bar\n' + 254 | '··baz\n' 255 | ); 256 | }); 257 | 258 | lines('should allow for dangling "Expected" block', () => { 259 | assert.is( 260 | strip($.lines('foo\nbaz', 'foo\nbar\nbaz')), 261 | '··foo\n' + 262 | 'Expected:\n' + 263 | '++bar\n' + 264 | '··baz\n' 265 | ); 266 | }); 267 | 268 | lines('should accept line numbers', () => { 269 | assert.is( 270 | strip($.lines('foo\nbar', 'foo\nbat', 1)), 271 | 'L1 ··foo\n' + 272 | 'Actual:\n' + 273 | 'L2 --bar\n' + 274 | 'Expected:\n' + 275 | 'L2 ++bat\n' 276 | ); 277 | }); 278 | 279 | lines('should handle line numbers with num-digits change', () => { 280 | assert.is( 281 | strip($.lines( 282 | '1\n2\n3\n4\n5\n6\n7\n8a\n9a\n10a\n11\n12\n13', 283 | '1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13', 1 284 | )), 285 | 'L01 ··1\n' + 286 | 'L02 ··2\n' + 287 | 'L03 ··3\n' + 288 | 'L04 ··4\n' + 289 | 'L05 ··5\n' + 290 | 'L06 ··6\n' + 291 | 'L07 ··7\n' + 292 | 'Actual:\n' + 293 | 'L08 --8a\n' + 294 | 'L09 --9a\n' + 295 | 'L10 --10a\n' + 296 | 'Expected:\n' + 297 | 'L08 ++8\n' + 298 | 'L09 ++9\n' + 299 | 'L10 ++10\n' + 300 | 'L11 ··11\n' + 301 | 'L12 ··12\n' + 302 | 'L13 ··13\n' 303 | ); 304 | }); 305 | 306 | lines('should track "expected" for line numbers', () => { 307 | assert.is( 308 | strip($.lines('foo\nbaz', 'foo\nbar\nbaz', 1)), 309 | 'L1 ··foo\n' + 310 | 'Expected:\n' + 311 | 'L2 ++bar\n' + 312 | 'L3 ··baz\n' 313 | ); 314 | 315 | assert.is( 316 | strip($.lines('foo\nbar\nbaz', 'foo\nbaz', 1)), 317 | 'L1 ··foo\n' + 318 | 'Actual:\n' + 319 | 'L2 --bar\n' + 320 | 'L2 ··baz\n' 321 | ); 322 | }); 323 | 324 | lines('should retain new lines ("↵") differences', () => { 325 | assert.is( 326 | strip($.lines('foo\nbaz', 'foo\n\n\nbaz')), 327 | '··foo\n' + 328 | 'Expected:\n' + 329 | '++↵\n' + 330 | '++↵\n' + 331 | '··baz\n' 332 | ); 333 | 334 | assert.is( 335 | strip($.lines('foo\nbaz', 'foo\n\n\nbaz', 1)), 336 | 'L1 ··foo\n' + 337 | 'Expected:\n' + 338 | 'L2 ++↵\n' + 339 | 'L3 ++↵\n' + 340 | 'L4 ··baz\n' 341 | ); 342 | }); 343 | 344 | lines.run(); 345 | 346 | // --- 347 | 348 | const chars = suite('chars'); 349 | 350 | chars('should be a function', () => { 351 | assert.type($.chars, 'function'); 352 | }); 353 | 354 | // TODO: return nothing ? 355 | chars.skip('should no differences with identical text', () => { 356 | // assert.fixture(); 357 | }); 358 | 359 | chars('should handle `"foo"` vs `"fo"` diff', () => { 360 | assert.is( 361 | strip($.chars('foo', 'fo')), 362 | '++fo (Expected)\n' + 363 | '--foo (Actual)\n'+ 364 | ' ^' 365 | ); 366 | }); 367 | 368 | chars('should handle `"fo"` vs `"foo"` diff', () => { 369 | assert.is( 370 | strip($.chars('fo', 'foo')), 371 | '++foo (Expected)\n' + 372 | '--fo (Actual)\n'+ 373 | ' ^' 374 | ); 375 | }); 376 | 377 | chars('should handle `"foo"` vs `"bar"` diff', () => { 378 | assert.is( 379 | strip($.chars('foo', 'bar')), 380 | '++bar (Expected)\n' + 381 | '--foo (Actual)\n' + 382 | ' ^^^' 383 | ); 384 | }); 385 | 386 | chars('should handle `"foobar"` vs `"foobaz"` diff', () => { 387 | assert.is( 388 | strip($.chars('foobar', 'foobaz')), 389 | '++foobaz (Expected)\n' + 390 | '--foobar (Actual)\n' + 391 | ' ^' 392 | ); 393 | }); 394 | 395 | chars('should handle two `Date.toISOString()` diff', () => { 396 | assert.is( 397 | strip($.chars('2019-12-23T01:26:30.092Z', '2020-06-23T01:45:31.268Z')), 398 | '++2020-06-23T01:45:31.268Z (Expected)\n' + 399 | '--2019-12-23T01:26:30.092Z (Actual)\n' + 400 | ' ^^ ^^ ^^ ^ ^^^ ' 401 | ); 402 | 403 | assert.is( 404 | strip($.chars('2019-12-23T01:26:30.098Z', '2020-06-23T01:45:31.268Z')), 405 | '++2020-06-23T01:45:31.268Z (Expected)\n' + 406 | '--2019-12-23T01:26:30.098Z (Actual)\n' + 407 | ' ^^ ^^ ^^ ^ ^^ ' 408 | ); 409 | 410 | assert.is( 411 | strip($.chars('2020-09-23T01:45:31.268Z', '2020-06-23T01:45:31.268Z')), 412 | '++2020-06-23T01:45:31.268Z (Expected)\n' + 413 | '--2020-09-23T01:45:31.268Z (Actual)\n' + 414 | ' ^ ' 415 | ); 416 | }); 417 | 418 | chars('should handle `"help"` vs `"hello"` diff', () => { 419 | assert.is( 420 | strip($.chars('help', 'hello')), 421 | '++hello (Expected)\n' + 422 | '--help (Actual)\n' + 423 | ' ^^' 424 | ); 425 | }); 426 | 427 | chars('should handle `"yellow"` vs `"hello"` diff', () => { 428 | assert.is( 429 | strip($.chars('yellow', 'hello')), 430 | '++hello (Expected)\n' + 431 | '--yellow (Actual)\n' + 432 | ' ^ ^' 433 | ); 434 | 435 | assert.is( 436 | strip($.chars('hello', 'yellow')), 437 | '++yellow (Expected)\n' + 438 | '--hello (Actual)\n' + 439 | ' ^ ^' 440 | ); 441 | }); 442 | 443 | chars('should handle shared prefix', () => { 444 | assert.is( 445 | strip($.chars('abc123', 'abc1890')), 446 | '++abc1890 (Expected)\n' + 447 | '--abc123 (Actual)\n' + 448 | ' ^^^' 449 | ); 450 | 451 | assert.is( 452 | strip($.chars('abc1890', 'abc123')), 453 | '++abc123 (Expected)\n' + 454 | '--abc1890 (Actual)\n' + 455 | ' ^^^' 456 | ); 457 | 458 | assert.is( 459 | strip($.chars('abc1890', 'abc1234')), 460 | '++abc1234 (Expected)\n' + 461 | '--abc1890 (Actual)\n' + 462 | ' ^^^' 463 | ); 464 | }); 465 | 466 | chars('should handle shared suffix', () => { 467 | assert.is( 468 | strip($.chars('123xyz', '00xyz')), 469 | '++ 00xyz (Expected)\n' + 470 | '--123xyz (Actual)\n' + 471 | ' ^^^ ' 472 | ); 473 | 474 | assert.is( 475 | strip($.chars('00xyz', '123xyz')), 476 | '++123xyz (Expected)\n' + 477 | '-- 00xyz (Actual)\n' + 478 | ' ^^^ ' 479 | ); 480 | 481 | assert.is( 482 | strip($.chars('000xyz', '123xyz')), 483 | '++123xyz (Expected)\n' + 484 | '--000xyz (Actual)\n' + 485 | ' ^^^ ' 486 | ); 487 | }); 488 | 489 | chars('should handle shared middle', () => { 490 | assert.is( 491 | strip($.chars('123xyz456', '789xyz000')), 492 | '++789xyz000 (Expected)\n' + 493 | '--123xyz456 (Actual)\n' + 494 | ' ^^^ ^^^' 495 | ); 496 | 497 | assert.is( 498 | strip($.chars('123xyz45', '789xyz000')), 499 | '++789xyz000 (Expected)\n' + 500 | '--123xyz45 (Actual)\n' + 501 | ' ^^^ ^^^' 502 | ); 503 | 504 | assert.is( 505 | strip($.chars('23xyz45', '789xyz000')), 506 | '++789xyz000 (Expected)\n' + 507 | '-- 23xyz45 (Actual)\n' + 508 | ' ^^^ ^^^' 509 | ); 510 | }); 511 | 512 | chars('should print "·" character for space', () => { 513 | assert.is( 514 | strip($.chars('foo bar', 'foo bar baz')), 515 | '++foo·bar·baz (Expected)\n' + 516 | '--foo·bar (Actual)\n' + 517 | ' ^^^^' 518 | ); 519 | 520 | assert.is( 521 | strip($.chars('foo bar', 'foo bar')), 522 | '++foo·bar (Expected)\n' + 523 | '--foo···bar (Actual)\n' + 524 | ' ^^ ' 525 | ); 526 | }); 527 | 528 | chars('should print "→" character for tab', () => { 529 | assert.is( 530 | strip($.chars('foo bar\tbaz \t', 'foo bar\tbaz \t\t bat')), 531 | '++foo·bar→baz·→→·bat (Expected)\n' + 532 | '--foo·bar→baz·→ (Actual)\n' + 533 | ' ^^^^^' 534 | ); 535 | 536 | assert.is( 537 | strip($.chars('foo bar\tbaz \t\t bat', 'foo bar\tbaz \t')), 538 | '++foo·bar→baz·→ (Expected)\n' + 539 | '--foo·bar→baz·→→·bat (Actual)\n' + 540 | ' ^^^^^' 541 | ); 542 | 543 | assert.is( 544 | strip($.chars('foo\tbar\tbaz', 'foo bar baz')), 545 | '++foo·bar·baz (Expected)\n' + 546 | '--foo→bar→baz (Actual)\n' + 547 | ' ^ ^ ' 548 | ); 549 | }); 550 | 551 | chars('should handle empty string', () => { 552 | assert.is( 553 | strip($.chars('foo bar', '')), 554 | '++ (Expected)\n' + 555 | '--foo·bar (Actual)\n' + 556 | ' ^^^^^^^' 557 | ); 558 | 559 | assert.is( 560 | strip($.chars('', 'foo bar')), 561 | '++foo·bar (Expected)\n' + 562 | '-- (Actual)\n' + 563 | ' ^^^^^^^' 564 | ); 565 | }) 566 | 567 | chars.run(); 568 | 569 | // --- 570 | 571 | const direct = suite('direct'); 572 | 573 | direct('should be a function', () => { 574 | assert.type($.direct, 'function'); 575 | }); 576 | 577 | // TODO: return nothing ? 578 | direct.skip('should no differences with identical text', () => { 579 | // assert.fixture(); 580 | }); 581 | 582 | direct('should handle `"foo"` vs `"fo"` diff', () => { 583 | assert.snapshot( 584 | strip($.direct('foo', 'fo')), 585 | '++fo (Expected)\n' + 586 | '--foo (Actual)\n' 587 | ); 588 | }); 589 | 590 | direct('should handle `"fo"` vs `"foo"` diff', () => { 591 | assert.snapshot( 592 | strip($.direct('fo', 'foo')), 593 | '++foo (Expected)\n' + 594 | '--fo (Actual)\n' 595 | ); 596 | }); 597 | 598 | direct('should handle `"foo"` vs `"bar"` diff', () => { 599 | assert.snapshot( 600 | strip($.direct('foo', 'bar')), 601 | '++bar (Expected)\n' + 602 | '--foo (Actual)\n' 603 | ); 604 | }); 605 | 606 | direct('should handle `123` vs `456` diff', () => { 607 | assert.snapshot( 608 | strip($.direct(123, 456)), 609 | '++456 (Expected)\n' + 610 | '--123 (Actual)\n' 611 | ); 612 | }); 613 | 614 | direct('should handle `123` vs `"123"` diff', () => { 615 | assert.snapshot( 616 | strip($.direct(123, '123')), 617 | '++123 [string] (Expected)\n' + 618 | '--123 [number] (Actual)\n' 619 | ); 620 | 621 | assert.snapshot( 622 | strip($.direct('123', 123)), 623 | '++123 [number] (Expected)\n' + 624 | '--123 [string] (Actual)\n' 625 | ); 626 | }); 627 | 628 | direct('should handle `12` vs `"123"` diff', () => { 629 | assert.snapshot( 630 | strip($.direct(12, '123')), 631 | '++123 [string] (Expected)\n' + 632 | '--12 [number] (Actual)\n' 633 | ); 634 | 635 | assert.snapshot( 636 | strip($.direct('123', 12)), 637 | '++12 [number] (Expected)\n' + 638 | '--123 [string] (Actual)\n' 639 | ); 640 | }); 641 | 642 | direct('should handle `null` vs `"null"` diff', () => { 643 | assert.snapshot( 644 | strip($.direct(null, 'null')), 645 | '++null [string] (Expected)\n' + 646 | '--null [object] (Actual)\n' 647 | ); 648 | 649 | assert.snapshot( 650 | strip($.direct('null', null)), 651 | '++null [object] (Expected)\n' + 652 | '--null [string] (Actual)\n' 653 | ); 654 | }); 655 | 656 | direct('should handle `true` vs `"true"` diff', () => { 657 | assert.snapshot( 658 | strip($.direct(true, 'true')), 659 | '++true [string] (Expected)\n' + 660 | '--true [boolean] (Actual)\n' 661 | ); 662 | 663 | assert.snapshot( 664 | strip($.direct('true', true)), 665 | '++true [boolean] (Expected)\n' + 666 | '--true [string] (Actual)\n' 667 | ); 668 | }); 669 | 670 | direct('should handle `false` vs `"true"` diff', () => { 671 | assert.snapshot( 672 | strip($.direct(false, 'true')), 673 | '++true [string] (Expected)\n' + 674 | '--false [boolean] (Actual)\n' 675 | ); 676 | 677 | assert.snapshot( 678 | strip($.direct('true', false)), 679 | '++false [boolean] (Expected)\n' + 680 | '--true [string] (Actual)\n' 681 | ); 682 | }); 683 | 684 | direct.run(); 685 | 686 | // --- 687 | 688 | const compare = suite('compare'); 689 | 690 | compare('should be a function', () => { 691 | assert.type($.compare, 'function'); 692 | }); 693 | 694 | compare('should proxy `$.arrays` for Array inputs', () => { 695 | assert.is( 696 | strip($.compare([1, 2, 3], [1, 2, 4])), 697 | '··[\n' + 698 | '····1,\n' + 699 | '····2,\n' + 700 | 'Actual:\n' + 701 | '--··3,\n' + 702 | 'Expected:\n' + 703 | '++··4,\n' + 704 | '··]\n' 705 | ); 706 | }); 707 | 708 | compare('should proxy `$.chars` for RegExp inputs', () => { 709 | assert.is( 710 | strip($.compare(/foo/g, /foobar/gi)), 711 | '++/foobar/gi (Expected)\n' + 712 | '--/foo/g (Actual)\n' + 713 | ' ^^^ ^' 714 | ); 715 | 716 | assert.is( 717 | strip($.compare(/foobar/gi, /foo/g)), 718 | '++/foo/g (Expected)\n' + 719 | '--/foobar/gi (Actual)\n' + 720 | ' ^^^ ^' 721 | ); 722 | }); 723 | 724 | compare('should proxy `$.lines` for Object inputs', () => { 725 | assert.is( 726 | strip($.compare({ foo: 1 }, { foo: 2, bar: 3 })), 727 | '··{\n' + 728 | 'Actual:\n' + 729 | '--··"foo":·1\n' + 730 | 'Expected:\n' + 731 | '++··"foo":·2,\n' + 732 | '++··"bar":·3\n' + 733 | '··}\n' 734 | ); 735 | 736 | assert.is( 737 | strip($.compare({ foo: 2, bar: 3 }, { foo: 1 })), 738 | '··{\n' + 739 | 'Actual:\n' + 740 | '--··"foo":·2,\n' + 741 | '--··"bar":·3\n' + 742 | 'Expected:\n' + 743 | '++··"foo":·1\n' + 744 | '··}\n' 745 | ); 746 | 747 | assert.is( 748 | strip($.compare({ foo: 2, bar: undefined, baz: NaN }, { foo: 1, bar: null })), 749 | '··{\n' + 750 | 'Actual:\n' + 751 | '--··"foo":·2,\n' + 752 | '--··"bar":·undefined,\n' + 753 | '--··"baz":·NaN\n' + 754 | 'Expected:\n' + 755 | '++··"foo":·1,\n' + 756 | '++··"bar":·null\n' + 757 | '··}\n' 758 | ); 759 | 760 | assert.is( 761 | strip($.compare({ foo: 2, bar: null, baz: NaN }, { foo: 2, bar: undefined, baz: NaN })), 762 | '··{\n' + 763 | '····"foo":·2,\n' + 764 | 'Actual:\n' + 765 | '--··"bar":·null,\n' + 766 | 'Expected:\n' + 767 | '++··"bar":·undefined,\n' + 768 | '····"baz":·NaN\n' + 769 | '··}\n' 770 | ); 771 | }); 772 | 773 | compare('should proxy `$.lines` for multi-line String inputs', () => { 774 | assert.is( 775 | strip($.compare('foo\nbar', 'foo\nbat')), 776 | '··foo\n' + 777 | 'Actual:\n' + 778 | '--bar\n' + 779 | 'Expected:\n' + 780 | '++bat\n' 781 | ); 782 | }); 783 | 784 | compare('should proxy `$.chars` for single-line String inputs', () => { 785 | assert.is( 786 | strip($.compare('foobar', 'foobaz')), 787 | '++foobaz (Expected)\n' + 788 | '--foobar (Actual)\n' + 789 | ' ^' 790 | ); 791 | }); 792 | 793 | compare('should proxy `$.direct` for Number inputs', () => { 794 | assert.snapshot( 795 | strip($.compare(123, 12345)), 796 | '++12345 (Expected)\n' + 797 | '--123 (Actual)\n' 798 | ); 799 | 800 | assert.snapshot( 801 | strip($.compare(123, NaN)), 802 | '++NaN (Expected)\n' + 803 | '--123 (Actual)\n' 804 | ); 805 | 806 | assert.snapshot( 807 | strip($.compare(NaN, 123)), 808 | '++123 (Expected)\n' + 809 | '--NaN (Actual)\n' 810 | ); 811 | }); 812 | 813 | compare('should proxy `$.direct` for Boolean inputs', () => { 814 | assert.snapshot( 815 | strip($.compare(true, false)), 816 | '++false (Expected)\n' + 817 | '--true (Actual)\n' 818 | ); 819 | }); 820 | 821 | compare('should handle string against non-type mismatch', () => { 822 | assert.snapshot( 823 | strip($.compare('foobar', null)), 824 | '++null [object] (Expected)\n' + 825 | '--foobar [string] (Actual)\n' 826 | ); 827 | 828 | assert.snapshot( 829 | strip($.compare(null, 'foobar')), 830 | '++foobar [string] (Expected)\n' + 831 | '--null [object] (Actual)\n' 832 | ); 833 | 834 | assert.snapshot( 835 | strip($.compare('foobar', 123)), 836 | '++123 [number] (Expected)\n' + 837 | '--foobar [string] (Actual)\n' 838 | ); 839 | 840 | assert.snapshot( 841 | strip($.compare(123, 'foobar')), 842 | '++foobar [string] (Expected)\n' + 843 | '--123 [number] (Actual)\n' 844 | ); 845 | 846 | assert.snapshot( 847 | strip($.compare('foobar', undefined)), 848 | '++undefined [undefined] (Expected)\n' + 849 | '--foobar [string] (Actual)\n' 850 | ); 851 | 852 | assert.snapshot( 853 | strip($.compare(undefined, 'foobar')), 854 | '++foobar [string] (Expected)\n' + 855 | '--undefined [undefined] (Actual)\n' 856 | ); 857 | 858 | assert.snapshot( 859 | strip($.compare(NaN, undefined)), 860 | '++undefined [undefined] (Expected)\n' + 861 | '--NaN [number] (Actual)\n' 862 | ); 863 | 864 | assert.snapshot( 865 | strip($.compare(undefined, NaN)), 866 | '++NaN [number] (Expected)\n' + 867 | '--undefined [undefined] (Actual)\n' 868 | ); 869 | }); 870 | 871 | compare('should handle multi-line string against non-type mismatch', () => { 872 | assert.snapshot( 873 | strip($.compare('foo\nbar', null)), 874 | 'Actual:\n' + 875 | '--foo\n' + 876 | '--bar\n' + 877 | 'Expected:\n' + 878 | '++null\n' 879 | ); 880 | 881 | assert.snapshot( 882 | strip($.compare(null, 'foo\nbar')), 883 | 'Actual:\n' + 884 | '--null\n' + 885 | 'Expected:\n' + 886 | '++foo\n' + 887 | '++bar\n' 888 | ); 889 | 890 | assert.snapshot( 891 | strip($.compare('foo\nbar', 123)), 892 | 'Actual:\n' + 893 | '--foo\n' + 894 | '--bar\n' + 895 | 'Expected:\n' + 896 | '++123\n' 897 | ); 898 | 899 | assert.snapshot( 900 | strip($.compare(123, 'foo\nbar')), 901 | 'Actual:\n' + 902 | '--123\n' + 903 | 'Expected:\n' + 904 | '++foo\n' + 905 | '++bar\n' 906 | ); 907 | 908 | assert.snapshot( 909 | strip($.compare('foo\nbar', undefined)), 910 | 'Actual:\n' + 911 | '--foo\n' + 912 | '--bar\n' + 913 | 'Expected:\n' + 914 | '++undefined\n' 915 | ); 916 | 917 | assert.snapshot( 918 | strip($.compare(undefined, 'foo\nbar')), 919 | 'Actual:\n' + 920 | '--undefined\n' + 921 | 'Expected:\n' + 922 | '++foo\n' + 923 | '++bar\n' 924 | ); 925 | }); 926 | 927 | compare('should handle `null` vs object', () => { 928 | assert.snapshot( 929 | strip($.compare(null, { foo: 123 })), 930 | 'Actual:\n' + 931 | '--null\n' + 932 | 'Expected:\n' + 933 | '++{\n' + 934 | '++··"foo":·123\n' + 935 | '++}\n' 936 | ); 937 | 938 | assert.snapshot( 939 | strip($.compare({ foo: 123 }, null)), 940 | 'Actual:\n' + 941 | '--{\n' + 942 | '--··"foo":·123\n' + 943 | '--}\n' + 944 | 'Expected:\n' + 945 | '++null\n' 946 | ); 947 | }); 948 | 949 | compare('should handle `undefined` vs object', () => { 950 | assert.snapshot( 951 | strip($.compare(undefined, { foo: 123 })), 952 | 'Actual:\n' + 953 | '--undefined\n' + 954 | 'Expected:\n' + 955 | '++{\n' + 956 | '++··"foo":·123\n' + 957 | '++}\n' 958 | ); 959 | 960 | assert.snapshot( 961 | strip($.compare({ foo: 123 }, undefined)), 962 | 'Actual:\n' + 963 | '--{\n' + 964 | '--··"foo":·123\n' + 965 | '--}\n' + 966 | 'Expected:\n' + 967 | '++undefined\n' 968 | ); 969 | }); 970 | 971 | compare.run(); 972 | 973 | // --- 974 | 975 | const sort = suite('sort()'); 976 | 977 | sort('should ignore Date instances', () => { 978 | assert.equal($.sort({}, new Date), {}); 979 | assert.equal($.sort(new Date, new Date), {}); 980 | assert.equal($.sort(new Date, {}), {}); 981 | }); 982 | 983 | sort('should ignore RegExp instances', () => { 984 | assert.equal($.sort({}, /foo/), {}); 985 | assert.equal($.sort(/foo/, /foo/), {}); 986 | assert.equal($.sort(/foo/, {}), {}); 987 | }); 988 | 989 | sort('should ignore Set instances', () => { 990 | assert.equal($.sort({}, new Set), {}); 991 | assert.equal($.sort(new Set, new Set), {}); 992 | assert.equal($.sort(new Set, {}), {}); 993 | }); 994 | 995 | sort('should ignore Map instances', () => { 996 | assert.equal($.sort({}, new Map), {}); 997 | assert.equal($.sort(new Map, new Map), {}); 998 | assert.equal($.sort(new Map, {}), {}); 999 | }); 1000 | 1001 | sort('should align `input` to `expect` key order', () => { 1002 | assert.equal( 1003 | $.sort({ b: 2, a: 1 }, { a: 1, b: 2 }), 1004 | { a: 1, b: 2 } 1005 | ); 1006 | }); 1007 | 1008 | sort('should append extra `input` keys', () => { 1009 | assert.equal( 1010 | $.sort({ c: 3, b: 2, a: 1 }, { a: 1 }), 1011 | { a: 1, c: 3, b: 2 } 1012 | ); 1013 | }); 1014 | 1015 | sort('should omit missing `expect` keys', () => { 1016 | assert.equal( 1017 | $.sort({ c: 3, a: 1 }, { a: 1, b: 2, c: 3 }), 1018 | { a: 1, c: 3 } 1019 | ); 1020 | }); 1021 | 1022 | sort('should loop through Arrays for nested sorts', () => { 1023 | assert.equal( 1024 | $.sort([ 1025 | { a2: 2, a1: 1 }, 1026 | { b3: 3, b2: 2 }, 1027 | ], [ 1028 | { a1: 1, a2: 2, a3: 3 }, 1029 | { b1: 1, b2: 2, b3: 3 }, 1030 | ]), 1031 | [ 1032 | { a1: 1, a2: 2 }, 1033 | { b2: 2, b3: 3 }, 1034 | ] 1035 | ); 1036 | }); 1037 | 1038 | sort('should handle nested Object sorts', () => { 1039 | assert.equal( 1040 | $.sort({ 1041 | bar: { b:2, a:1 }, 1042 | foo: { c:3, b:2, a:1 }, 1043 | }, { 1044 | foo: { b:2, c:3 }, 1045 | bar: { b:2 }, 1046 | }), 1047 | { 1048 | foo: { b:2, c:3, a:1 }, 1049 | bar: { b:2, a:1 }, 1050 | } 1051 | ); 1052 | }); 1053 | 1054 | sort('should handle Object dictionary', () => { 1055 | let input = Object.create(null); 1056 | let expect = Object.create(null); 1057 | 1058 | input.aaa = 123; 1059 | input.bbb = 123; 1060 | input.ccc = 123; 1061 | 1062 | expect.ccc = 123; 1063 | expect.aaa = 123; 1064 | expect.bbb = 123; 1065 | 1066 | assert.equal( 1067 | $.sort(input, expect), 1068 | { ccc: 123, bbb: 123, aaa: 123 } 1069 | ); 1070 | }); 1071 | 1072 | sort.run(); 1073 | 1074 | // --- 1075 | 1076 | const circular = suite('circular'); 1077 | 1078 | circular('should ignore non-object values', () => { 1079 | const input = { a:1, b:2, c:'c', d:null, e:()=>{} }; 1080 | 1081 | assert.is( 1082 | JSON.stringify(input, $.circular()), 1083 | '{"a":1,"b":2,"c":"c","d":null}' 1084 | ); 1085 | }); 1086 | 1087 | circular('should retain `undefined` and `NaN` values', () => { 1088 | const input = { a:1, b:undefined, c:NaN }; 1089 | 1090 | assert.is( 1091 | JSON.stringify(input, $.circular()), 1092 | '{"a":1,"b":"[__VOID__]","c":"[__NAN__]"}' 1093 | ); 1094 | 1095 | assert.is( 1096 | JSON.stringify(input), 1097 | '{"a":1,"c":null}' 1098 | ); 1099 | }); 1100 | 1101 | circular('should replace circular references with "[Circular]" :: Object', () => { 1102 | const input = { a:1, b:2 }; 1103 | input.self = input; 1104 | 1105 | assert.is( 1106 | JSON.stringify(input, $.circular()), 1107 | '{"a":1,"b":2,"self":"[Circular]"}' 1108 | ); 1109 | 1110 | assert.throws( 1111 | () => JSON.stringify(input), 1112 | 'Converting circular structure to JSON' 1113 | ); 1114 | 1115 | assert.is( 1116 | JSON.stringify({ aaa: input, bbb: 123 }, $.circular()), 1117 | '{"aaa":{"a":1,"b":2,"self":"[Circular]"},"bbb":123}' 1118 | ); 1119 | }); 1120 | 1121 | circular('should replace circular references with "[Circular]" :: Array', () => { 1122 | const input = { a:1, b:2 }; 1123 | input.self = input; 1124 | 1125 | assert.is( 1126 | JSON.stringify([input], $.circular()), 1127 | '[{"a":1,"b":2,"self":"[Circular]"}]' 1128 | ); 1129 | 1130 | assert.throws( 1131 | () => JSON.stringify(input), 1132 | 'Converting circular structure to JSON' 1133 | ); 1134 | 1135 | assert.is( 1136 | JSON.stringify([{ aaa:1 }, { aaa:input }], $.circular()), 1137 | '[{"aaa":1},{"aaa":{"a":1,"b":2,"self":"[Circular]"}}]', 1138 | ); 1139 | }); 1140 | 1141 | circular.run(); 1142 | 1143 | // --- 1144 | 1145 | const stringify = suite('stringify'); 1146 | 1147 | stringify('should wrap `JSON.stringify` native', () => { 1148 | const input = { a:1, b:2, c:'c', d:null, e:()=>{} }; 1149 | 1150 | assert.is( 1151 | $.stringify(input), 1152 | JSON.stringify(input, null, 2) 1153 | ); 1154 | }); 1155 | 1156 | stringify('should retain `undefined` and `NaN` values :: Object', () => { 1157 | assert.is( 1158 | $.stringify({ a: 1, b: undefined, c: NaN }), 1159 | '{\n "a": 1,\n "b": undefined,\n "c": NaN\n}' 1160 | ); 1161 | }); 1162 | 1163 | // In ES6, array holes are treated like `undefined` values 1164 | stringify('should retain `undefined` and `NaN` values :: Array', () => { 1165 | assert.is( 1166 | $.stringify([1, undefined, 2, , 3, NaN, 4, 5]), 1167 | '[\n 1,\n undefined,\n 2,\n undefined,\n 3,\n NaN,\n 4,\n 5\n]' 1168 | ); 1169 | }); 1170 | 1171 | if (!isNode8) { 1172 | // Not currently supporting :: Object(BigInt(3)) && Object(4n) 1173 | stringify('should handle `BigInt` values correctly', () => { 1174 | let bigint = eval('100n'); // avoid Node8 syntax error 1175 | assert.is($.stringify(BigInt(1)), '"1"'); 1176 | assert.is($.stringify(bigint), '"100"'); 1177 | assert.is( 1178 | $.stringify([BigInt(1), bigint]), 1179 | '[\n "1",\n "100"\n]' 1180 | ); 1181 | }); 1182 | } 1183 | 1184 | stringify.run(); 1185 | -------------------------------------------------------------------------------- /test/exit.fails.js: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | 3 | // TODO: test.fail() modifier (see #47) 4 | 5 | // A test that calls process.exit must fail (even if it exits with 0). 6 | // Otherwise all tests might not run, or even all assertions within test. 7 | test('should fail if `process.exit` encountered', async () => { 8 | process.exit(0); 9 | }); 10 | 11 | // This promise will never resolve & the process will exit early. 12 | // This must fail, otherwise all tests/assertions might not run. 13 | test('should fail if Promise never resolves :: GC', async () => { 14 | await new Promise(() => {}); 15 | }); 16 | 17 | test.run(); 18 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const kleur = require('kleur'); 2 | const assert = require('assert'); 3 | const { totalist } = require('totalist/sync'); 4 | const { spawnSync } = require('child_process'); 5 | 6 | let code = 0; 7 | const LEFT = kleur.dim().red(' || '); 8 | const HOOK = ['-r', 'esm', '-r', 'module-alias/register']; 9 | const PASS = kleur.green().bold('[PASS] '); 10 | const FAIL = kleur.red().bold('[FAIL] '); 11 | 12 | totalist(__dirname, (rel, abs) => { 13 | if (rel === 'index.js') return; 14 | let pid = spawnSync('node', HOOK.concat(abs)); 15 | let file = kleur.bold().underline(rel); 16 | 17 | if (rel.endsWith('.fails.js')) { 18 | try { 19 | assert.notEqual(pid.status, 0, 'expected to fail'); 20 | assert.equal(pid.stderr.length > 0, true, 'run w/ stderr'); 21 | assert.equal(pid.stdout.length, 0, 'run w/o stdout'); 22 | console.log(PASS + file); 23 | } catch (err) { 24 | console.error(FAIL + file + ' :: "%s"', err.message); 25 | if (pid.stdout.length) { 26 | console.error(LEFT + '\n' + LEFT + pid.stdout.toString().replace(/(\r?\n)/g, '$1' + LEFT)); 27 | } 28 | code = 1; 29 | } 30 | return; 31 | } 32 | 33 | try { 34 | assert.equal(pid.status, 0, 'run w/o error code'); 35 | assert.equal(pid.stderr.length, 0, 'run w/o stderr'); 36 | assert.equal(pid.stdout.length > 0, true, 'run w/ stdout'); 37 | console.log(PASS + file); 38 | } catch (err) { 39 | console.error(FAIL + file + ' :: "%s"', err.message); 40 | if (pid.stdout.length) { 41 | console.error(LEFT + '\n' + LEFT + pid.stdout.toString().replace(/(\r?\n)/g, '$1' + LEFT)); 42 | } 43 | code = 1; 44 | } 45 | }); 46 | 47 | process.exit(code); 48 | -------------------------------------------------------------------------------- /test/parse.js: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import { readdirSync } from 'fs'; 3 | import { isAbsolute } from 'path'; 4 | import * as assert from 'uvu/assert'; 5 | import * as $ from '../parse'; 6 | 7 | const FILES = readdirSync(__dirname); 8 | 9 | const parse = suite('parse'); 10 | 11 | parse('should be a function', () => { 12 | assert.type($.parse, 'function'); 13 | }); 14 | 15 | parse('should rely on defaults', async () => { 16 | // dirname to avoid node_modules 17 | let output = await $.parse(__dirname); 18 | 19 | assert.type(output, 'object'); 20 | assert.is(output.dir, __dirname); 21 | assert.is(output.requires, false); 22 | 23 | assert.instance(output.suites, Array); 24 | assert.is(output.suites.length, FILES.length); 25 | 26 | output.suites.forEach(suite => { 27 | assert.is.not(isAbsolute(suite.name)); 28 | assert.is(FILES.includes(suite.name), true, '~> suite.name is relative filename') 29 | assert.is(isAbsolute(suite.file), true, '~> suite.file is absolute path'); 30 | }); 31 | }); 32 | 33 | parse.run(); 34 | 35 | // --- 36 | 37 | const dir = suite('dir'); 38 | 39 | dir('should accept relative `dir` input', async () => { 40 | let output = await $.parse('test'); 41 | 42 | assert.type(output, 'object'); 43 | assert.is(output.dir, __dirname); 44 | assert.is(output.requires, false); 45 | 46 | assert.instance(output.suites, Array); 47 | assert.is(output.suites.length, FILES.length); 48 | 49 | output.suites.forEach(suite => { 50 | assert.is.not(isAbsolute(suite.name)); 51 | assert.is(FILES.includes(suite.name), true, '~> suite.name is relative filename') 52 | assert.is(isAbsolute(suite.file), true, '~> suite.file is absolute path'); 53 | }); 54 | }); 55 | 56 | dir.run(); 57 | 58 | // --- 59 | 60 | const pattern = suite('pattern'); 61 | 62 | pattern('should only load tests matching pattern :: RegExp', async () => { 63 | let foo = await $.parse(__dirname, /assert/); 64 | assert.is(foo.suites[0].name, 'assert.js'); 65 | assert.is(foo.suites.length, 1); 66 | 67 | let bar = await $.parse(__dirname, /^uvu\.js$/); 68 | assert.is(bar.suites[0].name, 'uvu.js'); 69 | assert.is(bar.suites.length, 1); 70 | }); 71 | 72 | pattern('should only load tests matching pattern :: string', async () => { 73 | let foo = await $.parse(__dirname, 'assert'); 74 | assert.is(foo.suites[0].name, 'assert.js'); 75 | assert.is(foo.suites.length, 1); 76 | 77 | let bar = await $.parse(__dirname, '^uvu\\.js$'); 78 | assert.is(bar.suites[0].name, 'uvu.js'); 79 | assert.is(bar.suites.length, 1); 80 | }); 81 | 82 | pattern.run(); 83 | 84 | // --- 85 | 86 | const cwd = suite('options.cwd'); 87 | 88 | cwd('should affect from where `dir` resolves', async () => { 89 | let foo = await $.parse('.', '', { cwd: __dirname }); 90 | assert.is(foo.suites.length, FILES.length); 91 | foo.suites.forEach(suite => { 92 | assert.is(FILES.includes(suite.name), true, '~> suite.name is relative filename') 93 | assert.is(isAbsolute(suite.file), true, '~> suite.file is absolute path'); 94 | }); 95 | }); 96 | 97 | cwd.run(); 98 | 99 | // --- 100 | 101 | const ignore = suite('options.ignore'); 102 | 103 | ignore('should ignore test files matching :: RegExp', async () => { 104 | let foo = await $.parse(__dirname, '', { ignore: /assert/ }); 105 | assert.is(foo.suites.find(x => x.name === 'assert.js'), undefined); 106 | assert.is(foo.suites.length, FILES.length - 1); 107 | 108 | let bar = await $.parse(__dirname, '', { ignore: /^uvu\.js$/ }); 109 | assert.is(bar.suites.find(x => x.name === 'uvu.js'), undefined); 110 | assert.is(bar.suites.length, FILES.length - 1); 111 | }); 112 | 113 | ignore('should ignore test files matching :: RegExp', async () => { 114 | let foo = await $.parse(__dirname, '', { ignore: 'assert' }); 115 | assert.is(foo.suites.find(x => x.name === 'assert.js'), undefined); 116 | assert.is(foo.suites.length, FILES.length - 1); 117 | 118 | let bar = await $.parse(__dirname, '', { ignore: 'uvu.js' }); 119 | assert.is(bar.suites.find(x => x.name === 'uvu.js'), undefined); 120 | assert.is(bar.suites.length, FILES.length - 1); 121 | }); 122 | 123 | ignore.run(); 124 | 125 | // --- 126 | 127 | const requires = suite('options.require'); 128 | 129 | requires('should throw on invalid value(s)', async () => { 130 | try { 131 | await $.parse(__dirname, '', { require: ['foobar'] }); 132 | assert.unreachable('should have thrown'); 133 | } catch (err) { 134 | assert.instance(err, Error); 135 | assert.match(err.message, `Cannot find module "foobar"`); 136 | } 137 | }); 138 | 139 | requires('should `require` valid value(s)', async () => { 140 | let foo = await $.parse(__dirname, '', { require: ['esm'] }); 141 | assert.is(foo.requires, true); 142 | }); 143 | 144 | requires.run(); 145 | -------------------------------------------------------------------------------- /test/suite.js: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | 4 | const hooks = suite('hooks'); 5 | 6 | const hooks_state = { 7 | before: 0, 8 | after: 0, 9 | each: 0, 10 | }; 11 | 12 | hooks.before(() => { 13 | assert.is(hooks_state.before, 0); 14 | assert.is(hooks_state.after, 0); 15 | assert.is(hooks_state.each, 0); 16 | hooks_state.before++; 17 | }); 18 | 19 | hooks.after(() => { 20 | assert.is(hooks_state.before, 1); 21 | assert.is(hooks_state.after, 0); 22 | assert.is(hooks_state.each, 0); 23 | hooks_state.after++; 24 | }); 25 | 26 | hooks.before.each(() => { 27 | assert.is(hooks_state.before, 1); 28 | assert.is(hooks_state.after, 0); 29 | assert.is(hooks_state.each, 0); 30 | hooks_state.each++; 31 | }); 32 | 33 | hooks.after.each(() => { 34 | assert.is(hooks_state.before, 1); 35 | assert.is(hooks_state.after, 0); 36 | assert.is(hooks_state.each, 1); 37 | hooks_state.each--; 38 | }); 39 | 40 | hooks('test #1', () => { 41 | assert.is(hooks_state.before, 1); 42 | assert.is(hooks_state.after, 0); 43 | assert.is(hooks_state.each, 1); 44 | }); 45 | 46 | hooks('test #2', () => { 47 | assert.is(hooks_state.before, 1); 48 | assert.is(hooks_state.after, 0); 49 | assert.is(hooks_state.each, 1); 50 | }); 51 | 52 | hooks.run(); 53 | 54 | hooks('ensure after() ran', () => { 55 | assert.is(hooks_state.before, 1); 56 | assert.is(hooks_state.after, 1); 57 | assert.is(hooks_state.each, 0); 58 | }); 59 | 60 | hooks.run(); 61 | 62 | // --- 63 | 64 | const skips = suite('suite.skip()'); 65 | 66 | const skips_state = { 67 | each: 0, 68 | }; 69 | 70 | skips.before.each(() => { 71 | skips_state.each++; 72 | }); 73 | 74 | skips('normal #1', () => { 75 | assert.ok('i should run'); 76 | assert.is(skips_state.each, 1); 77 | }); 78 | 79 | skips.skip('literal', () => { 80 | assert.unreachable('i should not run'); 81 | }); 82 | 83 | skips('normal #2', () => { 84 | assert.ok('but i should'); 85 | assert.is(skips_state.each, 2, 'did not run hook for skipped function'); 86 | }); 87 | 88 | skips.run(); 89 | 90 | // --- 91 | 92 | const only = suite('suite.only()'); 93 | 94 | const only_state = { 95 | each: 0, 96 | }; 97 | 98 | only.before.each(() => { 99 | only_state.each++; 100 | }); 101 | 102 | only('normal', () => { 103 | assert.unreachable('i should not run'); 104 | }); 105 | 106 | only.skip('modifier: skip', () => { 107 | assert.unreachable('i should not run'); 108 | }); 109 | 110 | only.only('modifier: only #1', () => { 111 | assert.ok('i should run'); 112 | assert.is(only_state.each, 1, 'did not run normal or skipped tests'); 113 | }); 114 | 115 | only.only('modifier: only #2', () => { 116 | assert.ok('i should also run'); 117 | assert.is(only_state.each, 2, 'did not run normal or skipped tests'); 118 | }); 119 | 120 | only.run(); 121 | 122 | // --- 123 | 124 | const context1 = suite('context #1'); 125 | 126 | context1.before(ctx => { 127 | assert.is(ctx.before, undefined); 128 | assert.is(ctx.after, undefined); 129 | assert.is(ctx.each, undefined); 130 | 131 | Object.assign(ctx, { 132 | before: 1, 133 | after: 0, 134 | each: 0, 135 | }); 136 | }); 137 | 138 | context1.after(ctx => { 139 | assert.is(ctx.before, 1); 140 | assert.is(ctx.after, 0); 141 | assert.is(ctx.each, 0); 142 | ctx.after++; 143 | }); 144 | 145 | context1.before.each(ctx => { 146 | assert.is(ctx.before, 1); 147 | assert.is(ctx.after, 0); 148 | assert.is(ctx.each, 0); 149 | ctx.each++; 150 | }); 151 | 152 | context1.after.each(ctx => { 153 | assert.is(ctx.before, 1); 154 | assert.is(ctx.after, 0); 155 | assert.is(ctx.each, 1); 156 | ctx.each--; 157 | }); 158 | 159 | context1('test #1', ctx => { 160 | assert.is(ctx.before, 1); 161 | assert.is(ctx.after, 0); 162 | assert.is(ctx.each, 1); 163 | }); 164 | 165 | context1('test #2', ctx => { 166 | assert.is(ctx.before, 1); 167 | assert.is(ctx.after, 0); 168 | assert.is(ctx.each, 1); 169 | }); 170 | 171 | context1.run(); 172 | 173 | context1('ensure after() ran', ctx => { 174 | assert.is(ctx.before, 1); 175 | assert.is(ctx.after, 1); 176 | assert.is(ctx.each, 0); 177 | }); 178 | 179 | context1.run(); 180 | 181 | // --- 182 | 183 | const context2 = suite('context #2', { 184 | before: 0, 185 | after: 0, 186 | each: 0, 187 | }); 188 | 189 | context2.before(ctx => { 190 | assert.is(ctx.before, 0); 191 | assert.is(ctx.after, 0); 192 | assert.is(ctx.each, 0); 193 | ctx.before++; 194 | }); 195 | 196 | context2.after(ctx => { 197 | assert.is(ctx.before, 1); 198 | assert.is(ctx.after, 0); 199 | assert.is(ctx.each, 0); 200 | ctx.after++; 201 | }); 202 | 203 | context2.before.each(ctx => { 204 | assert.is(ctx.before, 1); 205 | assert.is(ctx.after, 0); 206 | assert.is(ctx.each, 0); 207 | ctx.each++; 208 | }); 209 | 210 | context2.after.each(ctx => { 211 | assert.is(ctx.before, 1); 212 | assert.is(ctx.after, 0); 213 | assert.is(ctx.each, 1); 214 | ctx.each--; 215 | }); 216 | 217 | context2('test #1', ctx => { 218 | assert.is(ctx.before, 1); 219 | assert.is(ctx.after, 0); 220 | assert.is(ctx.each, 1); 221 | }); 222 | 223 | context2('test #2', ctx => { 224 | assert.is(ctx.before, 1); 225 | assert.is(ctx.after, 0); 226 | assert.is(ctx.each, 1); 227 | }); 228 | 229 | context2.run(); 230 | 231 | context2('ensure after() ran', ctx => { 232 | assert.is(ctx.before, 1); 233 | assert.is(ctx.after, 1); 234 | assert.is(ctx.each, 0); 235 | }); 236 | 237 | context2.run(); 238 | 239 | // --- 240 | 241 | const input = { 242 | a: 1, 243 | b: [2, 3, 4], 244 | c: { foo: 5 }, 245 | set: new Set([1, 2]), 246 | date: new Date(), 247 | map: new Map, 248 | }; 249 | 250 | const context3 = suite('context #3', input); 251 | 252 | context3('should access keys', ctx => { 253 | assert.is(ctx.a, input.a); 254 | assert.equal(ctx.b, input.b); 255 | assert.equal(ctx.c, input.c); 256 | }); 257 | 258 | context3('should allow context modifications', ctx => { 259 | ctx.a++; 260 | assert.is(ctx.a, 2); 261 | assert.is(input.a, 2); 262 | 263 | ctx.b.push(999); 264 | assert.equal(ctx.b, [2, 3, 4, 999]); 265 | assert.equal(input.b, [2, 3, 4, 999]); 266 | 267 | ctx.c.foo++; 268 | assert.is(ctx.c.foo, 6); 269 | assert.is(input.c.foo, 6); 270 | 271 | ctx.c.bar = 6; 272 | assert.equal(ctx.c, { foo: 6, bar: 6 }); 273 | assert.equal(input.c, { foo: 6, bar: 6 }); 274 | }); 275 | 276 | context3('should allow self-referencing instance(s) within context', ctx => { 277 | const { date, set, map } = ctx; 278 | 279 | assert.type(date.getTime(), 'number'); 280 | assert.equal([...set.values()], [1, 2]); 281 | assert.equal([...map.entries()], []); 282 | }); 283 | 284 | context3.run(); 285 | 286 | // --- 287 | 288 | const breadcrumbs = suite('breadcrumbs', { 289 | count: 1, 290 | }); 291 | 292 | breadcrumbs.before(ctx => { 293 | assert.is(ctx.__suite__, 'breadcrumbs'); 294 | assert.is(ctx.__test__, ''); 295 | }); 296 | 297 | breadcrumbs.after(ctx => { 298 | assert.is(ctx.__suite__, 'breadcrumbs'); 299 | assert.is(ctx.__test__, ''); 300 | }); 301 | 302 | breadcrumbs.before.each(ctx => { 303 | assert.is(ctx.__suite__, 'breadcrumbs'); 304 | assert.is(ctx.__test__, `test #${ctx.count}`); 305 | }); 306 | 307 | breadcrumbs.after.each(ctx => { 308 | assert.is(ctx.__suite__, 'breadcrumbs'); 309 | assert.is(ctx.__test__, `test #${ctx.count++}`); 310 | }); 311 | 312 | breadcrumbs('test #1', (ctx) => { 313 | assert.is(ctx.__suite__, 'breadcrumbs'); 314 | assert.is(ctx.__test__, 'test #1'); 315 | }); 316 | 317 | breadcrumbs('test #2', (ctx) => { 318 | assert.is(ctx.__suite__, 'breadcrumbs'); 319 | assert.is(ctx.__test__, 'test #2'); 320 | }); 321 | 322 | breadcrumbs.run(); 323 | -------------------------------------------------------------------------------- /test/uvu.js: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import * as uvu from '../src/index'; 4 | 5 | const ste = suite('suite'); 6 | 7 | ste('should be a function', () => { 8 | assert.type(uvu.suite, 'function'); 9 | }); 10 | 11 | ste.run(); 12 | 13 | // --- 14 | 15 | const test = suite('test'); 16 | 17 | test('should be a function', () => { 18 | assert.type(uvu.test, 'function'); 19 | }); 20 | 21 | test.run(); 22 | 23 | // --- 24 | 25 | const exec = suite('exec'); 26 | 27 | exec('should be a function', () => { 28 | assert.type(uvu.exec, 'function'); 29 | }); 30 | 31 | exec.run(); 32 | --------------------------------------------------------------------------------