diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dd84ea78..b5c68e55 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,7 +4,6 @@ about: Create a report to help us improve title: '' labels: '' assignees: '' - --- **Describe the bug** @@ -12,6 +11,7 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -24,15 +24,17 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] + +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] **Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] + +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- Browser [e.g. stock browser, safari] +- Version [e.g. 22] **Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index bbcbbe7d..2f28cead 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -4,7 +4,6 @@ about: Suggest an idea for this project title: '' labels: '' assignees: '' - --- **Is your feature request related to a problem? Please describe.** diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e24627c9..9c5135e3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,39 +4,65 @@ env: FORCE_COLOR: 1 on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] jobs: linux: - name: "Linux" + name: 'Linux' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: microsoft/playwright-github-action@v1 - - run: npm install - - run: npm run lint - - run: xvfb-run -a npm run test - - run: ./cli.js mocks/test.mocha.js --browser firefox - - run: ./cli.js mocks/test.mocha.js --browser webkit + - uses: actions/checkout@v2 + - uses: microsoft/playwright-github-action@v1 + - run: npm install + - run: npm run lint + - run: xvfb-run -a npm run test + - run: ./cli.js mocks/test.mocha.js --browser firefox + - run: ./cli.js mocks/test.mocha.js --browser webkit macos: - name: "Mac" + name: 'Mac' runs-on: macos-latest steps: - - uses: actions/checkout@v2 - - run: npm install - - run: npm test - - run: ./cli.js mocks/test.mocha.js --browser firefox - - run: ./cli.js mocks/test.mocha.js --browser webkit + - uses: actions/checkout@v2 + - run: npm install + - run: npm test + - run: ./cli.js mocks/test.mocha.js --browser firefox + - run: ./cli.js mocks/test.mocha.js --browser webkit win: - name: "Win" + name: 'Win' runs-on: windows-latest steps: - - uses: actions/checkout@v2 - - run: npm install - - run: npm test - - run: ./cli.js mocks/test.mocha.js --browser firefox - - run: ./cli.js mocks/test.mocha.js --browser webkit + - uses: actions/checkout@v2 + - run: npm install + - run: npm test + - run: ./cli.js mocks/test.mocha.js --browser firefox + - run: ./cli.js mocks/test.mocha.js --browser webkit + release: + runs-on: ubuntu-latest + needs: + - linux + - macos + - win + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + steps: + - uses: GoogleCloudPlatform/release-please-action@v2 + id: release + with: + token: ${{ secrets.GITHUB_TOKEN }} + release-type: node + - uses: actions/checkout@v2 + if: ${{ steps.release.outputs.release_created }} + - uses: actions/setup-node@v2 + with: + node-version: 14 + registry-url: 'https://registry.npmjs.org' + if: ${{ steps.release.outputs.release_created }} + - uses: bahmutov/npm-install@v1 + if: ${{ steps.release.outputs.release_created }} + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + if: ${{ steps.release.outputs.release_created }} diff --git a/.gitignore b/.gitignore index 616227b8..740b2c36 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ node_modules coverage.lcov yarn-error.log yarn.lock +dist diff --git a/cli.js b/cli.js index 0b8b87ea..66c90d05 100755 --- a/cli.js +++ b/cli.js @@ -1,51 +1,68 @@ #!/usr/bin/env node /* eslint-disable no-console */ -'use strict'; - -const path = require('path'); -const sade = require('sade'); -const kleur = require('kleur'); -const lilconfig = require('lilconfig'); -const merge = require('merge-options').bind({ ignoreUndefined: true }); -const pkg = require('./package.json'); -const { runnerOptions } = require('./src/utils'); -const UvuRunner = require('./src/runner-uvu'); -const MochaRunner = require('./src/runner-mocha'); -const TapeRunner = require('./src/runner-tape'); -const BenchmarkRunner = require('./src/runner-benchmark'); -const ZoraRunner = require('./src/runner-zora'); +'use strict' + +const path = require('path') +const sade = require('sade') +const kleur = require('kleur') +const lilconfig = require('lilconfig') +const merge = require('merge-options').bind({ ignoreUndefined: true }) +const pkg = require('./package.json') +const { runnerOptions } = require('./src/utils') +const UvuRunner = require('./src/runner-uvu') +const MochaRunner = require('./src/runner-mocha') +const TapeRunner = require('./src/runner-tape') +const BenchmarkRunner = require('./src/runner-benchmark') +const ZoraRunner = require('./src/runner-zora') // Handle any uncaught errors -process.once('uncaughtException', (err, origin) => { - if (!origin || origin === 'uncaughtException') { - console.error(err); - process.exit(1); - } -}); +process.once('uncaughtException', ( + /** @type {Error} */ err, + /** @type {string} */ origin +) => { + if (!origin || origin === 'uncaughtException') { + console.error(err) + process.exit(1) + } +}) process.once('unhandledRejection', (err) => { - console.error(err); - process.exit(1); -}); + console.error(err) + process.exit(1) +}) const extra = ` ${kleur.bold('Examples')} ${kleur.dim('$ playwright-test test.js --runner tape')} ${kleur.dim('$ playwright-test test --debug')} - ${kleur.dim('$ playwright-test "test/**/*.spec.js" --browser webkit --mode worker --incognito --debug')} + ${kleur.dim( + '$ playwright-test "test/**/*.spec.js" --browser webkit --mode worker --incognito --debug' + )} ${kleur.dim('$ playwright-test bench.js --runner benchmark')} - ${kleur.gray('# Uses benchmark.js to run your benchmark see playwright-test/mocks/benchmark.js for an example.')} - - ${kleur.dim('$ playwright-test test --cov && npx nyc report --reporter=html')} - ${kleur.gray('# Enable code coverage in istanbul format which can be used by nyc.')} - - ${kleur.dim('$ playwright-test "test/**/*.spec.js" --debug --before ./mocks/before.js')} - ${kleur.gray('# Run a script in a separate tab check ./mocks/before.js for an example.\n # Important: You need to call `self.PW_TEST.beforeEnd()` to start the main script.')} + ${kleur.gray( + '# Uses benchmark.js to run your benchmark see playwright-test/mocks/benchmark.js for an example.' + )} + + ${kleur.dim( + '$ playwright-test test --cov && npx nyc report --reporter=html' + )} + ${kleur.gray( + '# Enable code coverage in istanbul format which can be used by nyc.' + )} + + ${kleur.dim( + '$ playwright-test "test/**/*.spec.js" --debug --before ./mocks/before.js' + )} + ${kleur.gray( + '# Run a script in a separate tab check ./mocks/before.js for an example.\n # Important: You need to call `self.PW_TEST.beforeEnd()` to start the main script.' + )} ${kleur.bold('Runner Options')} All arguments passed to the cli not listed above will be fowarded to the runner. - ${kleur.dim('$ playwright-test test.js --runner mocha --bail --grep \'should fail\'')} + ${kleur.dim( + "$ playwright-test test.js --runner mocha --bail --grep 'should fail'" + )} To send a \`false\` flag use --no-bail. Check https://mochajs.org/api/mocha for \`mocha\` options or \`npx mocha --help\`. @@ -57,101 +74,124 @@ const extra = ` Do not let your shell expand globs, always wrap them. ${kleur.dim('$ playwright-test "test/**"')} GOOD ${kleur.dim('$ playwright-test test/**')} BAD -`; +` const sade2 = new Proxy(sade('playwright-test [files]', true), { - get: (target, prop, receiver) => { - const targetValue = Reflect.get(target, prop, receiver); + get: (target, prop, receiver) => { + const targetValue = Reflect.get(target, prop, receiver) - if (typeof targetValue === 'function') { - return function(...args) { - const out = targetValue.apply(this, args); + if (typeof targetValue === 'function') { + return function (/** @type {any} */ ...args) { + // @ts-ignore + const out = targetValue.apply(this, args) - if (prop === 'help') { - console.log(extra); - } - - return out; - }; + if (prop === 'help') { + console.log(extra) } - return targetValue; + return out + } } -}); + + return targetValue + }, +}) sade2 - .version(pkg.version) - .describe('Run mocha, zora, uvu, tape and benchmark.js scripts inside real browsers with `playwright`.') - .option('-r, --runner', 'Test runner. Options: mocha, tape, benchmark and zora.', 'mocha') - .option('-b, --browser', 'Browser to run tests. Options: chromium, firefox, webkit. (default chromium)') - .option('-m, --mode', 'Run mode. Options: main, worker. (default main)') - .option('-d, --debug', 'Debug mode, keeps browser window open.') - .option('-w, --watch', 'Watch files for changes and re-run tests.') - .option('-i, --incognito', 'Use incognito window to run tests.') - .option('-e, --extension', 'Use extension background_page to run tests.') - .option('--cov', 'Enable code coverage in istanbul format. Outputs \'.nyc_output/coverage-pw.json\'.') - .option('--before', 'Path to a script to be loaded on a separate tab before the main script.') - .option('--assets', 'Assets to be served by the http server. (default process.cwd())') - .option('--cwd', 'Current directory. (default process.cwd())') - .option('--extensions', 'File extensions allowed in the bundle. (default js,cjs,mjs,ts,tsx)') - .option('--config', 'Path to the config file') - .action((input, opts) => { - let config; - - if (opts.config) { - config = lilconfig.lilconfigSync('playwright-test').load(path.resolve(opts.config)); - } else { - config = lilconfig.lilconfigSync('playwright-test').search(); - if (!config) { - config = lilconfig.lilconfigSync('pw-test').search(); - } - } + .version(pkg.version) + .describe( + 'Run mocha, zora, uvu, tape and benchmark.js scripts inside real browsers with `playwright`.' + ) + .option( + '-r, --runner', + 'Test runner. Options: mocha, tape, benchmark and zora.', + 'mocha' + ) + .option( + '-b, --browser', + 'Browser to run tests. Options: chromium, firefox, webkit. (default chromium)' + ) + .option('-m, --mode', 'Run mode. Options: main, worker. (default main)') + .option('-d, --debug', 'Debug mode, keeps browser window open.') + .option('-w, --watch', 'Watch files for changes and re-run tests.') + .option('-i, --incognito', 'Use incognito window to run tests.') + .option('-e, --extension', 'Use extension background_page to run tests.') + .option( + '--cov', + "Enable code coverage in istanbul format. Outputs '.nyc_output/coverage-pw.json'." + ) + .option( + '--before', + 'Path to a script to be loaded on a separate tab before the main script.' + ) + .option( + '--assets', + 'Assets to be served by the http server. (default process.cwd())' + ) + .option('--cwd', 'Current directory. (default process.cwd())') + .option( + '--extensions', + 'File extensions allowed in the bundle. (default js,cjs,mjs,ts,tsx)' + ) + .option('--config', 'Path to the config file') + .action((input, opts) => { + let config + + if (opts.config) { + config = lilconfig + .lilconfigSync('playwright-test') + .load(path.resolve(opts.config)) + } else { + config = lilconfig.lilconfigSync('playwright-test').search() + if (!config) { + config = lilconfig.lilconfigSync('pw-test').search() + } + } - let Runner = null; - - switch (opts.runner) { - case 'uvu': - Runner = UvuRunner; - break; - case 'zora': - Runner = ZoraRunner; - break; - case 'mocha': - Runner = MochaRunner; - break; - case 'tape': - Runner = TapeRunner; - break; - case 'benchmark': - Runner = BenchmarkRunner; - break; - default: - console.error('Runner not supported: ', opts.runner); - process.exit(1); - } - const runner = new Runner(merge( - config ? config.config : {}, - { - cwd: opts.cwd, - assets: opts.assets, - browser: opts.browser, - debug: opts.debug, - mode: opts.mode, - incognito: opts.incognito, - input: input ? [input, ...opts._] : undefined, - extension: opts.extension, - runnerOptions: runnerOptions(opts), - before: opts.before, - node: opts.node, - cov: opts.cov, - extensions: opts.extensions - } - )); - - if (opts.watch) { - runner.watch(); - } else { - runner.run(); - } - }) - .parse(process.argv); + let Runner = null + + switch (opts.runner) { + case 'uvu': + Runner = UvuRunner + break + case 'zora': + Runner = ZoraRunner + break + case 'mocha': + Runner = MochaRunner + break + case 'tape': + Runner = TapeRunner + break + case 'benchmark': + Runner = BenchmarkRunner + break + default: + console.error('Runner not supported: ', opts.runner) + process.exit(1) + } + const runner = new Runner( + merge(config ? config.config : {}, { + cwd: opts.cwd, + assets: opts.assets, + browser: opts.browser, + debug: opts.debug, + mode: opts.mode, + incognito: opts.incognito, + input: input ? [input, ...opts._] : undefined, + extension: opts.extension, + runnerOptions: runnerOptions(opts), + before: opts.before, + node: opts.node, + cov: opts.cov, + extensions: opts.extensions, + }) + ) + + if (opts.watch) { + runner.watch() + } else { + runner.run() + } + }) + .parse(process.argv) diff --git a/mocks/before.js b/mocks/before.js index 39dc58a6..c6ae0ca7 100644 --- a/mocks/before.js +++ b/mocks/before.js @@ -1,13 +1,13 @@ -'use strict'; +'use strict' -const debug = require('debug'); +const debug = require('debug') -const error = debug('app:error'); +const error = debug('app:error') -console.log('\nRun before stuff'); +console.log('\nRun before stuff') -error('testing debug in before script'); +error('testing debug in before script') setTimeout(() => { - console.log('done'); - self.PW_TEST.beforeEnd(); -}); + console.log('done') + self.PW_TEST.beforeEnd() +}) diff --git a/mocks/benchmark.js b/mocks/benchmark.js index a7fbee3f..839bdda2 100644 --- a/mocks/benchmark.js +++ b/mocks/benchmark.js @@ -1,29 +1,29 @@ /* eslint-disable no-unused-expressions */ -'use strict'; +'use strict' -const Benchmark = require('benchmark'); +const Benchmark = require('benchmark') // import Benchmark from 'benchmark' -const suite = new Benchmark.Suite(); +const suite = new Benchmark.Suite() // add tests -suite.add('RegExp#test', () => { - /o/.test('Hello World!'); -}) - .add('String#indexOf', () => { - 'Hello World!'.indexOf('o') > -1; - }) - .add('String#match', () => { - Boolean('Hello World!'.match(/o/)); - }) -// add listeners - .on('cycle', (event) => { - console.log(String(event.target)); - }) - .on('complete', function() { - console.log('Fastest is ' + this.filter('fastest').map('name')); - }) -// run async - .run({ 'async': true }); - +suite + .add('RegExp#test', () => { + ;/o/.test('Hello World!') + }) + .add('String#indexOf', () => { + 'Hello World!'.indexOf('o') > -1 + }) + .add('String#match', () => { + Boolean('Hello World!'.match(/o/)) + }) + // add listeners + .on('cycle', (event) => { + console.log(String(event.target)) + }) + .on('complete', function () { + console.log('Fastest is ' + this.filter('fastest').map('name')) + }) + // run async + .run({ async: true }) diff --git a/mocks/lib.js b/mocks/lib.js index 50df691b..5d6d0730 100644 --- a/mocks/lib.js +++ b/mocks/lib.js @@ -1,11 +1,14 @@ import delay from 'delay' export const good = async () => { - await delay(100) - return 'good' + await delay(100) + return 'good' } export const bad = async () => { - await delay(100) - return 'bad' + // new Promise((resolve, reject) => { + // throw new Error('ooopps') + // }) + await delay(100) + return 'bad' } diff --git a/mocks/test.mocha.js b/mocks/test.mocha.js index f3bbebf2..fed32954 100644 --- a/mocks/test.mocha.js +++ b/mocks/test.mocha.js @@ -1,30 +1,30 @@ /* eslint-disable no-undef */ // eslint-disable-next-line strict -const assert = require('assert'); -const debug = require('debug')('app'); -const { good, bad } = require('./lib'); +const assert = require('assert') +const debug = require('debug')('app') +const { good, bad } = require('./lib') describe('Array', () => { - describe('#indexOf()', () => { - it('should return -1 when the value is not present', () => { - assert.equal([1, 2, 3].indexOf(4), -1); - }); + describe('#indexOf()', () => { + it('should return -1 when the value is not present', () => { + assert.equal([1, 2, 3].indexOf(4), -1) + }) - it('should fail ', () => { - assert.strictEqual([1, 2, 3].indexOf(2), 1); - }); + it('should fail ', () => { + assert.strictEqual([1, 2, 3].indexOf(2), 1) + }) - it('should pass with debug', () => { - assert.equal([1, 2, 3].indexOf(4), -1); - debug('test pass'); - }); + it('should pass with debug', () => { + assert.equal([1, 2, 3].indexOf(4), -1) + debug('test pass') + }) - it('should return "good"', async () => { - assert.strictEqual(await good(), 'good'); - }); + it('should return "good"', async () => { + assert.strictEqual(await good(), 'good') + }) - it('should return "bad"', async () => { - assert.strictEqual(await bad(), 'bad'); - }); - }); -}); + it('should return "bad"', async () => { + assert.strictEqual(await bad(), 'bad') + }) + }) +}) diff --git a/mocks/test.tape.js b/mocks/test.tape.js index e841157a..0ae4d858 100644 --- a/mocks/test.tape.js +++ b/mocks/test.tape.js @@ -1,18 +1,36 @@ /* eslint-disable no-undef */ // eslint-disable-next-line strict -const test = require('tape'); -const debug = require('debug'); +const test = require('tape') +const debug = require('debug') -const error = debug('app:error'); +const error = debug('app:error') test('timing test', (t) => { - t.equal(typeof Date.now, 'function'); - t.end(); -}); + t.equal(typeof Date.now, 'function') + t.end() +}) test('controller exists', (t) => { - error('testing debug'); - t.equal(typeof Date.now, 'function'); - t.end(); -}); + error('testing debug') + t.equal(typeof Date.now, 'function') + t.end() +}) +test('timing test 2', function (t) { + t.plan(2) + + t.equal(typeof Date.now, 'function') + var start = Date.now() + + setTimeout(function () { + t.ok(Date.now() - start >= 100) + }, 100) +}) + +test('test using promises', async function (t) { + async function run() { + return true + } + const result = await run() + t.ok(result) +}) diff --git a/mocks/test1.zora.js b/mocks/test1.zora.js index c74acaae..73db07e9 100644 --- a/mocks/test1.zora.js +++ b/mocks/test1.zora.js @@ -1,19 +1,19 @@ -'use strict'; +'use strict' -const { test } = require('zora'); +const { test } = require('zora') test('a first sub test', (t) => { - t.ok(true); + t.ok(true) - t.test('inside', (t) => { - t.ok(true); - }); -}); + t.test('inside', (t) => { + t.ok(true) + }) +}) test('a first sub test', (t) => { - t.ok(true); + t.ok(true) - t.test('inside', (t) => { - t.ok(false, 'oh no!'); - }); -}); + t.test('inside', (t) => { + t.ok(false, 'oh no!') + }) +}) diff --git a/mocks/test2.tape.js b/mocks/test2.tape.js index efa3e9f7..79aae5b0 100644 --- a/mocks/test2.tape.js +++ b/mocks/test2.tape.js @@ -1,9 +1,9 @@ /* eslint-disable no-undef */ // eslint-disable-next-line strict -const test = require('tape'); +const test = require('tape') test('timing test 2', (t) => { - t.plan(1); + t.plan(1) - t.equal(typeof Date.now, 'function'); -}); + t.equal(typeof Date.now, 'function') +}) diff --git a/mocks/test2.zora.js b/mocks/test2.zora.js index b3d55ed8..5e9251bb 100644 --- a/mocks/test2.zora.js +++ b/mocks/test2.zora.js @@ -1,40 +1,40 @@ // 'use strict'; // const { test, only } = require('zora'); -import {test, only} from 'zora'; +import { test, only } from 'zora' test('some grouped assertions', (t) => { - t.ok(true, 'true is truthy'); - t.equal('bar', 'bar', 'that both string are equivalent'); - t.isNot({}, {}, 'those are not the same reference'); -}); + t.ok(true, 'true is truthy') + t.equal('bar', 'bar', 'that both string are equivalent') + t.isNot({}, {}, 'those are not the same reference') +}) test('some grouped assertions', (t) => { - t.ok(true, 'true is truthy'); + t.ok(true, 'true is truthy') - t.test('a group inside another one', (t) => { - t.equal('bar', 'bar', 'that both string are equivalent'); - t.isNot({}, {}, 'those are not the same reference'); - }); -}); + t.test('a group inside another one', (t) => { + t.equal('bar', 'bar', 'that both string are equivalent') + t.isNot({}, {}, 'those are not the same reference') + }) +}) only('should run', (t) => { - t.ok(true, 'I ran'); + t.ok(true, 'I ran') - t.only('keep running', (t) => { - t.only('keeeeeep running', (t) => { - t.ok(true, ' I got there'); - }); - }); + t.only('keep running', (t) => { + t.only('keeeeeep running', (t) => { + t.ok(true, ' I got there') + }) + }) - t.test('should not run', (t) => { - t.fail('shouldn ot run'); - }); -}); + t.test('should not run', (t) => { + t.fail('shouldn ot run') + }) +}) only('should run but nothing inside', (t) => { - t.test('will not run', (t) => { - t.fail('should not run'); - }); - t.test('will not run', (t) => { - t.fail('should not run'); - }); -}); + t.test('will not run', (t) => { + t.fail('should not run') + }) + t.test('will not run', (t) => { + t.fail('should not run') + }) +}) diff --git a/mocks/uvu/test1.js b/mocks/uvu/test1.js index 5d975e74..f82f1225 100644 --- a/mocks/uvu/test1.js +++ b/mocks/uvu/test1.js @@ -1,18 +1,18 @@ /* eslint-disable no-empty-function */ -'use strict'; +'use strict' -const { test } = require('uvu'); -const delay = require('delay'); -const assert = require('uvu/assert'); +const { test } = require('uvu') +const delay = require('delay') +const assert = require('uvu/assert') test('sum', () => { - assert.type(() => {}, 'function'); - assert.is(3, 3); -}); + assert.type(() => {}, 'function') + assert.is(3, 3) +}) test('sum', async () => { - await delay(100); - assert.type(() => {}, 'function'); - assert.is(3, 3); -}); -test.run(); + await delay(100) + assert.type(() => {}, 'function') + assert.is(3, 3) +}) +test.run() diff --git a/mocks/uvu/test2.js b/mocks/uvu/test2.js index 4e7cae98..7085c52a 100644 --- a/mocks/uvu/test2.js +++ b/mocks/uvu/test2.js @@ -1,20 +1,20 @@ /* eslint-disable no-empty-function */ -'use strict'; +'use strict' -const { suite } = require('uvu'); -const delay = require('delay'); -const assert = require('uvu/assert'); +const { suite } = require('uvu') +const delay = require('delay') +const assert = require('uvu/assert') -const test = suite('suite'); +const test = suite('suite') test('sum', () => { - assert.type(() => {}, 'function'); - assert.is(3, 3); -}); + assert.type(() => {}, 'function') + assert.is(3, 3) +}) test('sum', async () => { - await delay(2000); - assert.type(() => {}, 'function'); - assert.is(3, 3); -}); -test.run(); + await delay(2000) + assert.type(() => {}, 'function') + assert.is(3, 3) +}) +test.run() diff --git a/package.json b/package.json index ec3c2ca5..b58167c0 100644 --- a/package.json +++ b/package.json @@ -1,95 +1,106 @@ { - "name": "playwright-test", - "version": "2.1.0", - "description": "Run mocha, zora, uvu, tape and benchmark.js scripts inside real browsers with playwright.", - "repository": "hugomrdias/playwright-test", - "author": "Hugo Dias (hugodias.me)", - "license": "MIT", - "main": "cli.js", - "bin": { - "playwright-test": "cli.js", - "pw-test": "cli.js" - }, - "engines": { - "node": ">=12" - }, - "scripts": { - "test": "uvu", - "lint": "eslint --cache *.js src/*.js" - }, - "files": [ - "static", - "src", - "index.js", - "cli.js" - ], - "keywords": [ - "playwright", - "test", - "tests", - "test-runner", - "mocha", - "tape", - "benchmark", - "benchmark.js", - "zora", - "coverage", - "istanbul", - "nyc", - "code coverage", - "uvu" - ], - "dependencies": { - "camelcase": "^6.2.0", - "delay": "^5.0.0", - "esbuild": "^0.9.2", - "globby": "^11.0.2", - "kleur": "^4.1.4", - "lilconfig": "^2.0.2", - "lodash": "^4.17.21", - "merge-options": "^3.0.4", - "ora": "^5.3.0", - "path-browserify": "^1.0.1", - "playwright-core": "1.9.2", - "polka": "^0.5.2", - "premove": "^3.0.1", - "process": "^0.11.10", - "sade": "^1.7.4", - "sirv": "^1.0.11", - "source-map": "^0.6.0", - "strip-ansi": "^6.0.0", - "tempy": "^1.0.0", - "test-exclude": "^6.0.0", - "v8-to-istanbul": "^7.1.0" - }, - "devDependencies": { - "assert": "^2.0.0", - "benchmark": "^2.1.4", - "debug": "^4.3.1", - "eslint": "^4.7.1", - "eslint-config-halo": "^2.3.3", - "execa": "^5.0.0", - "fresh-tape": "^5.1.1", - "lint-staged": "^10.5.3", - "mocha": "^8.3.2", - "np": "^7.3.0", - "tap-spec": "^5.0.0", - "uvu": "^0.5.1", - "zora": "^4.0.2" - }, - "eslintConfig": { - "extends": "halo/plugins", - "parserOptions": { - "sourceType": "script" - } - }, - "eslintIgnore": [ - "node_modules", - "coverage", - "dist", - "src/vendor/benchmark.js", - "src/vendor/source-map-support.js", - "node-globals.js", - "mocks" - ] + "name": "playwright-test", + "version": "2.1.0", + "description": "Run mocha, zora, uvu, tape and benchmark.js scripts inside real browsers with playwright.", + "repository": "hugomrdias/playwright-test", + "author": "Hugo Dias (hugodias.me)", + "license": "MIT", + "main": "src/runner.js", + "types": "dist/src/runner.d.ts", + "bin": { + "playwright-test": "cli.js", + "pw-test": "cli.js" + }, + "engines": { + "node": ">=12" + }, + "scripts": { + "prepare": "tsc && copyfiles 'src/**/*.d.ts' dist", + "test": "uvu", + "lint": "eslint **/*.js && prettier --check *.{js,ts,yml,json} --ignore-path .gitignore && tsc" + }, + "files": [ + "dist/src/runner.d.ts", + "dist/src/runner.d.ts.map", + "dist/src/types.d.ts", + "static", + "src", + "index.js", + "cli.js" + ], + "keywords": [ + "playwright", + "test", + "tests", + "test-runner", + "mocha", + "tape", + "benchmark", + "benchmark.js", + "zora", + "coverage", + "istanbul", + "nyc", + "code coverage", + "uvu" + ], + "dependencies": { + "@hd-forks/v8-to-istanbul": "7.1.1", + "camelcase": "^6.2.0", + "esbuild": "^0.11.5", + "globby": "^11.0.3", + "kleur": "^4.1.4", + "lilconfig": "^2.0.2", + "lodash": "^4.17.21", + "merge-options": "^3.0.4", + "ora": "^5.4.0", + "path-browserify": "^1.0.1", + "playwright-core": "1.10.0", + "polka": "^0.5.2", + "premove": "^3.0.1", + "process": "^0.11.10", + "sade": "^1.7.4", + "sirv": "^1.0.11", + "source-map": "0.6.1", + "strip-ansi": "^6.0.0", + "tempy": "^1.0.1", + "test-exclude": "^6.0.0" + }, + "devDependencies": { + "@types/polka": "^0.5.2", + "@types/sade": "^1.7.2", + "assert": "^2.0.0", + "benchmark": "^2.1.4", + "copyfiles": "^2.4.1", + "debug": "^4.3.1", + "delay": "^5.0.0", + "execa": "^5.0.0", + "fresh-tape": "^5.1.1", + "hd-scripts": "^0.1.1", + "mocha": "^8.3.2", + "np": "^7.3.0", + "tap-spec": "^5.0.0", + "uvu": "^0.5.1", + "zora": "^4.0.2" + }, + "simple-git-hooks": { + "pre-commit": "npx lint-staged" + }, + "lint-staged": { + "*.{js,ts,md,yml,json}": "prettier --write", + "*.js": "eslint --fix" + }, + "eslintConfig": { + "extends": "./node_modules/hd-scripts/eslint/index.js" + }, + "eslintIgnore": [ + "node_modules", + "coverage", + "dist", + "src/vendor/benchmark.js", + "src/vendor/source-map-support.js", + "node-globals.js", + "mocks" + ], + "prettier": "hd-scripts/prettier.config.js" } diff --git a/readme.md b/readme.md index 644caea3..ab395d2b 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,6 @@ > Run mocha, zora, uvu, tape and benchmark.js scripts inside real browsers with `playwright`. - ## Install ```shell @@ -10,12 +9,14 @@ $ npm install playwright-test ``` ## Usage + ```shell $ playwright-test [files] [options] -# or +# or $ pw-test [files] [options] ``` + ## Options ```shell @@ -73,8 +74,11 @@ Description $ playwright-test "test/**" GOOD $ playwright-test test/** BAD ``` + ## Config + Configuration can be done with cli flags or config files. + ```js 'package.json', // using property `pw-test` or `playwright-test` `.playwright-testrc.json`, @@ -89,9 +93,30 @@ Configuration can be done with cli flags or config files. `pw-test.config.cjs`, ``` -## Run in CI -Check our CI config `.github/workflows/main.yml` and the playwright Github Action https://playwright.dev/#version=v1.5.2&path=docs%2Fci.md&q=github-actions +Interface + +```ts +export interface RunnerOptions { + cwd?: string + assets?: string + browser?: 'chromium' | 'firefox' | 'webkit' + debug?: boolean + mode?: 'main' | 'worker' + incognito?: boolean + input?: string[] + extension?: boolean + runnerOptions?: any + before?: string + cov?: false + extensions?: string + // esbuild options @see https://esbuild.github.io/api/#simple-options + buildConfig: BuildOptions +} +``` + +## Run in CI +Check our CI config `.github/workflows/main.yml` and the playwright [Github Action](https://playwright.dev/docs/ci/#github-actions) ## License diff --git a/src/node-globals.js b/src/node-globals.js index ff8d7082..3a36eca3 100644 --- a/src/node-globals.js +++ b/src/node-globals.js @@ -1,2 +1,3 @@ +// @ts-nocheck export const process = require('process/browser') // https://github.com/ionic-team/rollup-plugin-node-polyfills diff --git a/src/runner-benchmark.js b/src/runner-benchmark.js index ec9625d3..ae1d306a 100644 --- a/src/runner-benchmark.js +++ b/src/runner-benchmark.js @@ -1,23 +1,30 @@ /* eslint-disable no-console */ -'use strict'; +'use strict' -const path = require('path'); -const Runner = require('./runner'); -const { build } = require('./utils'); +const path = require('path') +const Runner = require('./runner') +const { build } = require('./utils') class BenchmarkRunner extends Runner { - compiler() { - const plugin = { - name: 'swap benchmark', - setup(build) { - build.onResolve({ filter: /^benchmark$/ }, () => { - return { path: path.join(__dirname, 'setup-bench.js') }; - }); - } - }; - - return build(this, { plugins: [plugin] }); + /** + * Compile tests + * + * @param {"before" | "bundle" | "watch"} mode + * @returns {Promise} file to be loaded in the page + */ + compiler(mode) { + /** @type {import('esbuild').Plugin} */ + const plugin = { + name: 'swap benchmark', + setup(build) { + build.onResolve({ filter: /^benchmark$/ }, () => { + return { path: path.join(__dirname, 'setup-bench.js') } + }) + }, } + + return build(this, { plugins: [plugin] }) + } } -module.exports = BenchmarkRunner; +module.exports = BenchmarkRunner diff --git a/src/runner-mocha.js b/src/runner-mocha.js index 09b285af..58a267ca 100644 --- a/src/runner-mocha.js +++ b/src/runner-mocha.js @@ -1,17 +1,16 @@ /* eslint-disable no-console */ -'use strict'; +'use strict' -const merge = require('merge-options'); -const delay = require('delay'); -const Runner = require('./runner'); -const { build } = require('./utils'); +const merge = require('merge-options') +const Runner = require('./runner') +const { build } = require('./utils') const runMocha = () => ` mocha .run((f) =>{ self.PW_TEST.end(f > 0) }) -`; +` const runMochaWorker = () => ` mocha @@ -21,60 +20,62 @@ mocha "pwRunFailed": f > 0 }) }) -`; +` class MochaRunner extends Runner { - constructor(options = {}) { - super( - merge( - { - runnerOptions: { - allowUncaught: false, - bail: true, - reporter: 'spec', - timeout: 5000, - color: true, - ui: 'bdd' - } - }, - options - ) - ); - } - - async runTests() { - await super.runTests(); - switch (this.options.mode) { - case 'main': { - await this.page.evaluate(runMocha()); - break; - } - case 'worker': { - const run = new Promise((resolve) => { - this.page.on('worker', async (worker) => { - await delay(1000); - await worker.evaluate(runMochaWorker()); - resolve(); - }); - }); + constructor(options = {}) { + super( + merge( + { + runnerOptions: { + allowUncaught: false, + bail: true, + reporter: 'spec', + timeout: 5000, + color: true, + ui: 'bdd', + }, + }, + options + ) + ) + } - await run; - break; - } - default: - await this.stop(true, 'mode not supported'); - break; - } + /** + * @param {import("playwright-core").Page} page + * @param {string} file + */ + async runTests(page, file) { + await super.runTests(page, file) + switch (this.options.mode) { + case 'main': { + await page.evaluate(runMocha()) + break + } + case 'worker': { + const worker = await page.waitForEvent('worker') + await worker.evaluate(runMochaWorker()) + break + } + default: + throw Error('mode not supported') } + } - compiler(mode = 'bundle') { - return build( - this, - {}, - `require('${require.resolve('./setup-mocha.js').replace(/\\/g, '/')}')`, - mode - ); - } + /** + * Compile tests + * + * @param {"before" | "bundle" | "watch"} mode + * @returns {Promise} file to be loaded in the page + */ + compiler(mode = 'bundle') { + return build( + this, + {}, + `require('${require.resolve('./setup-mocha.js').replace(/\\/g, '/')}')`, + mode + ) + } } -module.exports = MochaRunner; +module.exports = MochaRunner diff --git a/src/runner-tape.js b/src/runner-tape.js index 85bd4438..f04b3fdd 100644 --- a/src/runner-tape.js +++ b/src/runner-tape.js @@ -1,27 +1,38 @@ /* eslint-disable no-console */ -'use strict'; +'use strict' -const Runner = require('./runner'); -const { build } = require('./utils'); +const Runner = require('./runner') +const { build } = require('./utils') -class TapeRunner extends Runner { - compiler(mode = 'bundle') { - const plugin = { - name: 'swap tape', - setup(build) { - build.onResolve({ filter: /^tape$/ }, () => { - return { path: require.resolve('fresh-tape') }; - }); - } - }; +/** + * @typedef {import('esbuild').Plugin} EsbuildPlugin + */ - return build( - this, - { plugins: [plugin] }, - `require('${require.resolve('./setup-tape.js').replace(/\\/g, '/')}')`, - mode - ); +class TapeRunner extends Runner { + /** + * Compile tests + * + * @param {"before" | "bundle" | "watch"} mode + * @returns {Promise} file to be loaded in the page + */ + compiler(mode = 'bundle') { + /** @type {EsbuildPlugin} */ + const plugin = { + name: 'swap tape', + setup(build) { + build.onResolve({ filter: /^tape$/ }, () => { + return { path: require.resolve('fresh-tape') } + }) + }, } + + return build( + this, + { plugins: [plugin] }, + `require('${require.resolve('./setup-tape.js').replace(/\\/g, '/')}')`, + mode + ) + } } -module.exports = TapeRunner; +module.exports = TapeRunner diff --git a/src/runner-uvu.js b/src/runner-uvu.js index 2353b274..60e87c70 100644 --- a/src/runner-uvu.js +++ b/src/runner-uvu.js @@ -1,37 +1,47 @@ /* eslint-disable no-console */ -'use strict'; +'use strict' -const strip = require('strip-ansi'); -const Runner = require('./runner'); -const { build } = require('./utils'); +const strip = require('strip-ansi') +const Runner = require('./runner') +const { build } = require('./utils') -const run = pass => ` +const run = (/** @type {boolean} */ pass) => ` self.PW_TEST.end(${pass}) -`; +` class UvuRunner extends Runner { - async runTests() { - await super.runTests(); - - let total = 0; - let passed = 0; - - this.page.on('console', async (msg) => { - const txt = msg.text(); - - if (txt.includes(' Total: ')) { - total = Number(txt.replace('Total:', '').trim()); - } - if (txt.includes(' Passed: ')) { - passed = Number(strip(txt.replace('Passed:', '').trim())); - await this.page.evaluate(run(total !== passed)); - } - }); - } - - compiler(mode = 'bundle') { - return build(this, {}, '', mode); - } + /** + * @param {import("playwright-core").Page} page + * @param {string} file + */ + async runTests(page, file) { + await super.runTests(page, file) + + let total = 0 + let passed = 0 + + page.on('console', async (msg) => { + const txt = msg.text() + + if (txt.includes(' Total: ')) { + total = Number(txt.replace('Total:', '').trim()) + } + if (txt.includes(' Passed: ')) { + passed = Number(strip(txt.replace('Passed:', '').trim())) + await page.evaluate(run(total !== passed)) + } + }) + } + + /** + * Compile tests + * + * @param {"before" | "bundle" | "watch"} mode + * @returns {Promise} file to be loaded in the page + */ + compiler(mode = 'bundle') { + return build(this, {}, '', mode) + } } -module.exports = UvuRunner; +module.exports = UvuRunner diff --git a/src/runner-zora.js b/src/runner-zora.js index 9bd44570..8fde6e86 100644 --- a/src/runner-zora.js +++ b/src/runner-zora.js @@ -1,10 +1,9 @@ /* eslint-disable no-console */ -'use strict'; +'use strict' -const path = require('path'); -const delay = require('delay'); -const Runner = require('./runner'); -const { build } = require('./utils'); +const path = require('path') +const Runner = require('./runner') +const { build } = require('./utils') const runZora = () => ` zora @@ -12,7 +11,7 @@ zora .then((f) =>{ self.PW_TEST.end(!self.zora.pass) }) -`; +` const runZoraWorker = () => ` zora @@ -23,55 +22,57 @@ zora "pwRunFailed": !self.zora.pass }) }) -`; +` class ZoraRunner extends Runner { - async runTests() { - await super.runTests(); - switch (this.options.mode) { - case 'main': { - await this.page.evaluate(runZora()); - break; - } - case 'worker': { - const run = new Promise((resolve) => { - this.page.on('worker', async (worker) => { - await delay(1000); - await worker.evaluate(runZoraWorker()); - resolve(); - }); - }); - - await run; - break; - } - default: - await this.stop(true, 'mode not supported'); - break; - } + /** + * @param {import("playwright-core").Page} page + * @param {string} file + */ + async runTests(page, file) { + await super.runTests(page, file) + switch (this.options.mode) { + case 'main': { + await page.evaluate(runZora()) + break + } + case 'worker': { + const worker = await page.waitForEvent('worker') + await worker.evaluate(runZoraWorker()) + break + } + default: + throw Error('mode not supported') } + } - compiler(mode = 'bundle') { - /** - * @type {import('esbuild').Plugin} build - */ - const plugin = { - name: 'swap zora', - setup(build) { - build.onResolve({ filter: /^zora$/ }, (args) => { - const setupPath = path.normalize('playwright-test/src/setup-zora.js'); - - if (args.importer.endsWith(setupPath)) { - return; - } + /** + * Compile tests + * + * @param {"before" | "bundle" | "watch"} mode + * @returns {Promise} file to be loaded in the page + */ + compiler(mode = 'bundle') { + /** + * @type {import('esbuild').Plugin} build + */ + const plugin = { + name: 'swap zora', + setup(build) { + build.onResolve({ filter: /^zora$/ }, (args) => { + const setupPath = path.normalize('playwright-test/src/setup-zora.js') - return { path: path.join(__dirname, 'setup-zora.js') }; - }); - } - }; + if (args.importer.endsWith(setupPath)) { + return + } - return build(this, { plugins: [plugin] }, '', mode); + return { path: path.join(__dirname, 'setup-zora.js') } + }) + }, } + + return build(this, { plugins: [plugin] }, '', mode) + } } -module.exports = ZoraRunner; +module.exports = ZoraRunner diff --git a/src/runner.js b/src/runner.js index abc9572d..e6fda3a8 100644 --- a/src/runner.js +++ b/src/runner.js @@ -1,339 +1,374 @@ -/* eslint-disable unicorn/no-process-exit */ /* eslint-disable no-console */ -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const ora = require('ora'); -const kleur = require('kleur'); -const tempy = require('tempy'); -const { premove } = require('premove/sync'); -const merge = require('merge-options').bind({ ignoreUndefined: true }); +'use strict' + +const fs = require('fs') +const path = require('path') +const ora = require('ora') +const tempy = require('tempy') +const { premove } = require('premove/sync') +const merge = require('merge-options').bind({ ignoreUndefined: true }) const { - redirectConsole, - getPw, - addWorker, - findTests, - defaultTestPatterns, - createCov, - createPolka -} = require('./utils'); + redirectConsole, + getPw, + addWorker, + findTests, + defaultTestPatterns, + createCov, + createPolka, +} = require('./utils') /** * @typedef {import('playwright-core').Page} Page * @typedef {import('playwright-core').BrowserContext} Context * @typedef {import('playwright-core').Browser} Browser + * @typedef {import('./types').RunnerOptions} RunnerOptions + * @typedef {import('playwright-core').ChromiumBrowserContext} ChromiumBrowserContext */ +/** + * @type {RunnerOptions} + */ const defaultOptions = { - cwd: process.cwd(), - assets: '', - browser: 'chromium', - debug: false, - mode: 'main', // worker - incognito: false, - input: null, - extension: false, - runnerOptions: {}, - before: null, - node: true, - cov: false, - extensions: 'js,cjs,mjs,ts,tsx', - buildConfig: {} -}; + cwd: process.cwd(), + assets: '', + browser: 'chromium', + debug: false, + mode: 'main', // worker + incognito: false, + input: undefined, + extension: false, + runnerOptions: {}, + before: undefined, + cov: false, + extensions: 'js,cjs,mjs,ts,tsx', + buildConfig: {}, +} class Runner { - constructor(options = {}) { - this.options = merge(defaultOptions, options); - this.server = null; - /** @type {Browser} */ - this.browser = null; - /** @type {Context} */ - this.context = null; - /** @type {Page} */ - this.page = null; - this.dir = tempy.directory(); - this.browserDir = tempy.directory(); - this.file = null; - this.url = ''; - this.stopped = false; - this.watching = false; - this.env = merge(JSON.parse(JSON.stringify(process.env)), { PW_TEST: this.options }); - this.extensions = this.options.extensions.split(','); - this.tests = findTests({ - cwd: this.options.cwd, - extensions: this.extensions, - filePatterns: this.options.input ? - this.options.input : - defaultTestPatterns(this.extensions) - }); - if (this.tests.length === 0) { - this.stop(false, 'No test files were found.'); - } + /** + * + * @param {Partial} [options] + */ + constructor(options = {}) { + /** @type {RunnerOptions} */ + this.options = merge(defaultOptions, options) + /** @type {import('polka').Polka["server"] | null} */ + this.server = null + this.dir = tempy.directory() + this.browserDir = tempy.directory() + this.url = '' + this.stopped = false + this.watching = false + this.env = merge(JSON.parse(JSON.stringify(process.env)), { + PW_TEST: this.options, + }) + this.extensions = this.options.extensions.split(',') + this.tests = findTests({ + cwd: this.options.cwd, + extensions: this.extensions, + filePatterns: this.options.input + ? this.options.input + : defaultTestPatterns(this.extensions), + }) + if (this.tests.length === 0) { + this.stop(false, 'No test files were found.') } - - async launch() { - // copy files to be served - const files = [ - 'index.html', - 'before.html', - 'favicon.ico', - 'manifest.json', - 'background.js', - 'setup.js' - ]; - - for (const file of files) { - fs.copyFileSync( - path.join(__dirname, './../static', file), - path.join(this.dir, file) - ); - } - - // setup http server - await createPolka(this); - - // download playwright if needed - if (!['chromium', 'firefox', 'webkit'].includes(String(this.options.browser))) { - throw new Error(`Browser not supported: ${this.options.browser}`); - } - const pw = await getPw(this.options.browser); - const pwOptions = { - headless: !this.options.extension && !this.options.debug, - devtools: this.options.browser === 'chromium' && this.options.debug, - args: this.options.extension ? - [ - `--disable-extensions-except=${this.dir}`, - `--load-extension=${this.dir}` - ] : - [], - dumpio: process.env.PW_TEST_DUMPIO || false - }; - - // create context - if (this.options.incognito) { - this.browser = await pw.launch(pwOptions); - this.context = await this.browser.newContext(); - } else { - this.context = await pw.launchPersistentContext( - this.browserDir, - pwOptions - ); - } - - return this; + } + + async launch() { + // copy files to be served + const files = [ + 'index.html', + 'before.html', + 'favicon.ico', + 'manifest.json', + 'background.js', + 'setup.js', + ] + + for (const file of files) { + fs.copyFileSync( + path.join(__dirname, './../static', file), + path.join(this.dir, file) + ) } - async setupPage() { - if (this.options.extension) { - const backgroundPages = await this.context.backgroundPages(); - const backgroundPage = backgroundPages.length ? - backgroundPages[0] : - await this.context - .waitForEvent('backgroundpage') - .then(event => event); - - this.page = backgroundPage; - if (this.options.debug) { - // Open extension devtools window - const extPage = await this.context.newPage(); - - await extPage.goto( - `chrome://extensions/?id=${ - this.page._mainFrame._initializer.url.split('/')[2] - }` - ); - - const buttonHandle = await extPage.evaluateHandle( - 'document.querySelector("body > extensions-manager").shadowRoot.querySelector("extensions-toolbar").shadowRoot.querySelector("#devMode")' - ); - - await buttonHandle.click(); - - const backgroundPageLink = await extPage.evaluateHandle( - 'document.querySelector("body > extensions-manager").shadowRoot.querySelector("#viewManager > extensions-detail-view").shadowRoot.querySelector("#inspect-views > li:nth-child(2) > a")' - ); - - await backgroundPageLink.click(); - } - } else if (this.options.incognito) { - this.page = await this.context.newPage(); - await this.page.goto(this.url); - } else { - this.page = await this.context.pages()[0]; - await this.page.goto(this.url); - } - - if (this.options.cov && this.options.mode === 'main') { - if (this.options.browser !== 'chromium') { - await this.stop(true, 'Coverage is only supported in chromium'); - } - await this.page.coverage.startJSCoverage(); - } - - this.page.on('crash', err => this.stop(true, err)); - this.page.on('error', err => this.stop(true, err)); - this.page.on('pageerror', (err) => { - console.error(err); - this.stop(true, 'Uncaught exception happened within the page. Run with --debug.'); - }); - this.page.on('console', redirectConsole); + // setup http server + await createPolka(this) + + // download playwright if needed + const pw = await getPw(this.options.browser) + /** @type {import('playwright-core').LaunchOptions} */ + const pwOptions = { + headless: !this.options.extension && !this.options.debug, + devtools: this.options.browser === 'chromium' && this.options.debug, + args: this.options.extension + ? [ + `--disable-extensions-except=${this.dir}`, + `--load-extension=${this.dir}`, + ] + : [], } - async runTests() { - await this.page.addScriptTag({ url: 'setup.js' }); - await this.page.evaluate(`localStorage.debug = "${this.env.DEBUG}"`); - - switch (this.options.mode) { - case 'main': { - await this.page.addScriptTag({ url: this.file }); - break; - } - case 'worker': { - this.page.evaluate(addWorker(this.file)); - break; - } - default: - await this.stop(true, 'mode not supported'); - break; - } + // create context + if (this.options.incognito) { + this.browser = await pw.launch(pwOptions) + this.context = await this.browser.newContext() + } else { + this.context = await pw.launchPersistentContext( + this.browserDir, + pwOptions + ) } - async waitForTestsToEnd() { - if (!this.options.debug) { - try { - await this.page.waitForFunction(() => self.PW_TEST.ended === true, undefined, { timeout: 0 }); - const testsFailed = await this.page.evaluate('self.PW_TEST.failed'); - - await this.stop(testsFailed); - } catch (err) { - if ( - err.message.includes('Protocol error (Runtime.callFunctionOn): Target closed') || - err.message.includes('Protocol error (Runtime.callFunctionOn): Browser closed') - ) { - console.error(kleur.yellow('\nBrowser was closed by an uncaught error.')); - } else { - this.stop(true, err); - } - } - } + return this.context + } + + /** + * Setup Page + * + * @param {Context} context + */ + async setupPage(context) { + if (this.options.extension && this.options.browser !== 'chromium') { + throw Error('Extension testing is only supported in chromium') } - async run() { - let spinner = ora('Setting up browser').start(); - - try { - await this.launch(); - if (this.options.before) { - await this.runBefore(); - } - await this.setupPage(); - spinner.succeed('Browser setup'); - - spinner = ora('Bundling tests').start(); - await this.compiler(); - spinner.succeed(); - await this.runTests(); - await this.waitForTestsToEnd(); - - // Re run on page reload - if (this.options.debug) { - this.page.on('load', async () => { - await this.runTests(); - await this.waitForTestsToEnd(); - }); - } - } catch (err) { - spinner.fail('Running tests failed.'); - await this.stop(true, err); - } + if (this.options.cov && this.options.browser !== 'chromium') { + throw Error('Coverage is only supported in chromium') } - async runBefore() { - // setup before page - this.pageBefore = await this.context.newPage(); - await this.pageBefore.goto(this.url + 'before.html'); - - // listen to errors - this.pageBefore.on('crash', (err) => { - this.stop(true, `Before page:\n ${err}`); - }); - this.pageBefore.on('error', (err) => { - this.stop(true, `Before page:\n ${err}`); - }); - this.pageBefore.on('pageerror', (err) => { - this.stop(true, `Before page:\n ${err}`); - }); - - // redirect console.log - this.pageBefore.on('console', redirectConsole); - try { - await this.compiler('before'); - await this.pageBefore.addScriptTag({ url: this.file }); - } catch (err) { - await this.stop(true, err); - } - - await this.pageBefore.waitForFunction('self.PW_TEST.beforeEnded', { timeout: 0 }); + if (this.options.cov && this.options.mode !== 'main') { + throw Error( + 'Coverage is only supported in the main thread use mode:"main" ' + ) } - async watch() { - const spinner = ora('Setting up browser').start(); - - await this.launch(); - if (this.options.before) { - spinner.text = 'Running before script'; - await this.runBefore(); - } - await this.setupPage(); - - spinner.succeed(); - - this.compiler('watch'); + if (this.options.extension) { + const context = /** @type {ChromiumBrowserContext} */ (this.context) + const backgroundPages = await context.backgroundPages() + this.page = backgroundPages.length + ? backgroundPages[0] + : await context.waitForEvent('backgroundpage') + + if (!this.page) { + throw Error('Could not find the background page for the extension.') + } + + if (this.options.debug) { + // Open extension devtools window + const extPage = await context.newPage() + + await extPage.goto( + `chrome://extensions/?id=${ + // @ts-ignore + this.page._mainFrame._initializer.url.split('/')[2] + }` + ) + + const buttonHandle = await extPage.evaluateHandle( + 'document.querySelector("body > extensions-manager").shadowRoot.querySelector("extensions-toolbar").shadowRoot.querySelector("#devMode")' + ) + + // @ts-ignore + await buttonHandle.click() + + const backgroundPageLink = await extPage.evaluateHandle( + 'document.querySelector("body > extensions-manager").shadowRoot.querySelector("#viewManager > extensions-detail-view").shadowRoot.querySelector("#inspect-views > li:nth-child(2) > a")' + ) + + // @ts-ignore + await backgroundPageLink.click() + } + } else if (this.options.incognito) { + this.page = await context.newPage() + await this.page.goto(this.url) + } else { + this.page = context.pages()[0] + await this.page.goto(this.url) } - async stop(fail, msg) { - if (this.stopped || this.options.debug) { - return; - } - this.stopped = true; + if (this.options.cov && this.page.coverage) { + await this.page.coverage.startJSCoverage() + } - if (this.options.cov && this.options.mode === 'main' && this.page && this.page.coverage) { - await createCov(this, await this.page.coverage.stopJSCoverage()); + this.page.on('console', redirectConsole) + return this.page + } + + /** + * Run the tests + * + * @param {Page} page + * @param {string} file - file to load in the page + */ + async runTests(page, file) { + await page.addScriptTag({ url: 'setup.js' }) + await page.evaluate(`localStorage.debug = "${this.env.DEBUG}"`) + + switch (this.options.mode) { + case 'main': { + await page.addScriptTag({ url: file }) + break + } + case 'worker': { + await page.evaluate(addWorker(file)) + break + } + default: + throw Error('mode not supported') + } + } + + /** + * Wait for tests to finish + * + * @param {Page} page + */ + async waitForTestsToEnd(page) { + await page.waitForFunction( + // @ts-ignore + () => self.PW_TEST.ended === true, + undefined, + { + timeout: 0, + polling: 100, // need to be polling raf doesnt work in extensions + } + ) + const testsFailed = await page.evaluate('self.PW_TEST.failed') + + await this.stop(testsFailed) + } + + async run() { + let spinner = ora('Setting up browser').start() + + try { + const context = await this.launch() + if (this.options.before) { + await this.runBefore(context) + } + const page = await this.setupPage(context) + // uncaught rejections + page.on('pageerror', (err) => { + console.error(err) + this.stop( + true, + 'Uncaught exception happened within the page. Run with --debug.' + ) + }) + spinner.succeed('Browser setup') + + spinner = ora('Bundling tests').start() + const file = await this.compiler() + spinner.succeed() + await this.runTests(page, file) + + // Re run on page reload + if (this.options.debug) { + page.on('load', async () => { + await this.runTests(page, file) + }) + } else { + await this.waitForTestsToEnd(page) + if (this.options.cov && page.coverage) { + await createCov(this, await page.coverage.stopJSCoverage(), file) } + } + } catch (err) { + spinner.fail('Running tests failed.') + await this.stop(true, err) + } + } + + /** + * Setup and run before page + * + * @param {Context} context + */ + async runBefore(context) { + const page = await context.newPage() + await page.goto(this.url + 'before.html') + + page.on('pageerror', (err) => { + this.stop(true, `Before page:\n ${err}`) + }) + + page.on('console', redirectConsole) + + const file = await this.compiler('before') + await page.addScriptTag({ url: file }) + await page.waitForFunction('self.PW_TEST.beforeEnded', { + timeout: 0, + }) + } + + async watch() { + const spinner = ora('Setting up browser').start() + + const context = await this.launch() + if (this.options.before) { + spinner.text = 'Running before script' + await this.runBefore(context) + } + const page = await this.setupPage(context) + page.on('pageerror', console.error) - if (this.context) { - await this.context.close(); - } + spinner.succeed() - if (this.server) { - const serverClose = new Promise((resolve, reject) => { - this.server.close((err) => { - if (err) { - return reject(err); - } - resolve(); - }); - }); - - await serverClose; - } + this.compiler('watch') + } - premove(this.dir); - premove(this.browserDir); + /** + * @param {boolean} fail + * @param {string | undefined} [msg] + */ + async stop(fail, msg) { + if (this.stopped || this.options.debug) { + return + } + this.stopped = true - if (fail && msg) { - console.error(msg); - } else if (msg) { - console.log(msg); - } - process.exit(fail ? 1 : 0); + if (this.context) { + await this.context.close() } - // eslint-disable-next-line no-unused-vars - async compiler(mode) { - // + const serverClose = new Promise((resolve, reject) => { + if (this.server) { + this.server.close((err) => { + if (err) { + return reject(err) + } + resolve(true) + }) + } else { + resolve(true) + } + }) + + await serverClose + + premove(this.dir) + premove(this.browserDir) + + if (fail && msg) { + console.error(msg) + } else if (msg) { + console.log(msg) } + process.exit(fail ? 1 : 0) + } + + /** + * Compile tests + * + * @param {"before" | "bundle" | "watch"} mode + * @returns {Promise} file to be loaded in the page + */ + async compiler(mode = 'bundle') { + // + throw new Error('abstract method') + } } -module.exports = Runner; +module.exports = Runner diff --git a/src/setup-bench.js b/src/setup-bench.js index 5cb102e1..5bf4a6ae 100644 --- a/src/setup-bench.js +++ b/src/setup-bench.js @@ -1,59 +1,60 @@ +// @ts-nocheck /* eslint-disable new-cap */ -'use strict'; +'use strict' // Run benchmarkjs in the browser https://github.com/bestiejs/benchmark.js/issues/128#issuecomment-271615298 // const process = require('process'); -const _ = require('lodash'); -const Benchmark = require('./vendor/benchmark'); +const _ = require('lodash') +const Benchmark = require('./vendor/benchmark') const BenchmarkSpecial = Benchmark.runInContext({ - _, - process -}); + _, + process, +}) -let runningCount = 0; +let runningCount = 0 const signalFinished = () => { - if (runningCount === 0) { - setTimeout(() => { - // eslint-disable-next-line no-undef - if (process.env.PW_TEST.mode === 'worker') { - postMessage({ 'pwRunEnded': true }); - } else { - self.PW_TEST.end(); - } - }, 1000); - } -}; + if (runningCount === 0) { + setTimeout(() => { + // eslint-disable-next-line no-undef + if (process.env.PW_TEST.mode === 'worker') { + postMessage({ pwRunEnded: true }) + } else { + self.PW_TEST.end() + } + }, 1000) + } +} const proxy = new Proxy(BenchmarkSpecial, { - get(obj, prop) { - if (prop === 'Suite') { - const SuiteProxy = new Proxy(obj.Suite, { - construct(target, args) { - const suite = new target(...args); - - suite.on('start', () => { - runningCount++; - }); - suite.on('complete', () => { - runningCount--; - signalFinished(); - }); - - return suite; - } - }); - - return SuiteProxy; - } - - if (prop in obj) { - return obj[prop]; - } + get(obj, prop) { + if (prop === 'Suite') { + const SuiteProxy = new Proxy(obj.Suite, { + construct(target, args) { + const suite = new target(...args) + + suite.on('start', () => { + runningCount++ + }) + suite.on('complete', () => { + runningCount-- + signalFinished() + }) + + return suite + }, + }) + + return SuiteProxy + } + + if (prop in obj) { + return obj[prop] } -}); + }, +}) -self.Benchmark = proxy; -module.exports = proxy; -exports.Benchmark = proxy; +self.Benchmark = proxy +module.exports = proxy +exports.Benchmark = proxy diff --git a/src/setup-mocha.js b/src/setup-mocha.js index 95ccf33a..6d738a18 100644 --- a/src/setup-mocha.js +++ b/src/setup-mocha.js @@ -1,26 +1,27 @@ -'use strict'; +// @ts-nocheck +'use strict' /* eslint-disable no-undef */ // mocha library itself, to have it set up on global -require('mocha/mocha'); +require('mocha/mocha') const { - allowUncaught, - bail, - reporter, - timeout, - color, - ui, - grep -} = process.env.PW_TEST.runnerOptions; + allowUncaught, + bail, + reporter, + timeout, + color, + ui, + grep, +} = process.env.PW_TEST.runnerOptions mocha.setup({ - allowUncaught, - bail, - reporter, - timeout, - color, - ui, - grep -}); + allowUncaught, + bail, + reporter, + timeout, + color, + ui, + grep, +}) diff --git a/src/setup-tape.js b/src/setup-tape.js index 18370ed0..92f9a66f 100644 --- a/src/setup-tape.js +++ b/src/setup-tape.js @@ -1,21 +1,22 @@ -'use strict'; +// @ts-nocheck +'use strict' -const test = require('fresh-tape'); +const test = require('fresh-tape') -self.TAPE_RUN_FAIL = false; +self.TAPE_RUN_FAIL = false test.onFailure(() => { - self.TAPE_RUN_FAIL = true; -}); + self.TAPE_RUN_FAIL = true +}) test.onFinish(() => { - // eslint-disable-next-line no-undef - if (process.env.PW_TEST.mode === 'worker') { - postMessage({ - 'pwRunEnded': true, - 'pwRunFailed': self.TAPE_RUN_FAIL - }); - } else { - self.PW_TEST.end(self.TAPE_RUN_FAIL); - } -}); + // eslint-disable-next-line no-undef + if (process.env.PW_TEST.mode === 'worker') { + postMessage({ + pwRunEnded: true, + pwRunFailed: self.TAPE_RUN_FAIL, + }) + } else { + self.PW_TEST.end(self.TAPE_RUN_FAIL) + } +}) diff --git a/src/setup-zora.js b/src/setup-zora.js index 6f527ce4..38d08962 100644 --- a/src/setup-zora.js +++ b/src/setup-zora.js @@ -1,13 +1,14 @@ -'use strict'; +'use strict' -const { createHarness } = require('zora'); +const { createHarness } = require('zora') const harness = createHarness({ - // eslint-disable-next-line no-undef - indent: process.env.INDENT === 'true', - // eslint-disable-next-line no-undef - runOnly: process.env.RUN_ONLY === 'true' -}); + // eslint-disable-next-line no-undef + indent: process.env.INDENT === 'true', + // eslint-disable-next-line no-undef + runOnly: process.env.RUN_ONLY === 'true', +}) -self.zora = harness; -module.exports = harness; +// @ts-ignore +self.zora = harness +module.exports = harness diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 00000000..98306a7f --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,30 @@ +import type { BuildOptions } from 'esbuild' +import type { + ChromiumBrowser, + FirefoxBrowser, + WebKitBrowser, +} from 'playwright-core' + +export interface RunnerOptions { + cwd: string + assets: string + browser: 'chromium' | 'firefox' | 'webkit' + debug: boolean + mode: 'main' | 'worker' + incognito: boolean + input?: string[] + extension: boolean + runnerOptions: any + before?: string + cov: false + extensions: string + buildConfig: BuildOptions +} + +export type PwResult = TBrowser extends 'webkit' + ? WebKitBrowser + : TBrowser extends 'firefox' + ? FirefoxBrowser + : TBrowser extends 'chromium' + ? ChromiumBrowser + : never diff --git a/src/utils.js b/src/utils.js index 92e54d47..b14dd8b5 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,384 +1,459 @@ -/* eslint-disable valid-jsdoc */ -/* eslint-disable camelcase */ -/* eslint-disable no-undefined */ -/* eslint-disable global-require */ /* eslint-disable no-console */ -'use strict'; - -const { createServer } = require('net'); -const path = require('path'); -const fs = require('fs'); -const { promisify } = require('util'); -const esbuild = require('esbuild'); -const kleur = require('kleur'); -const globby = require('globby'); -const ora = require('ora'); -const sirv = require('sirv'); -const polka = require('polka'); -const camelCase = require('camelcase'); -const V8ToIstanbul = require('v8-to-istanbul'); +'use strict' + +const { createServer } = require('net') +const path = require('path') +const fs = require('fs') +const { promisify } = require('util') +const esbuild = require('esbuild') +const kleur = require('kleur') +const globby = require('globby') +const ora = require('ora') +const sirv = require('sirv') +const polka = require('polka') +const camelCase = require('camelcase') +const V8ToIstanbul = require('@hd-forks/v8-to-istanbul') const merge = require('merge-options').bind({ - ignoreUndefined: true, - concatArrays: true -}); + ignoreUndefined: true, + concatArrays: true, +}) + +/** + * @typedef {import('./types').RunnerOptions } RunnerOptions + * @typedef {import('esbuild').Plugin} ESBuildPlugin + * @typedef {import('esbuild').BuildOptions} ESBuildOptions + */ -const writeFile = promisify(fs.writeFile); -const mkdir = promisify(fs.mkdir); +const writeFile = promisify(fs.writeFile) +const mkdir = promisify(fs.mkdir) const defaultIgnorePatterns = [ - '.git', // Git repository files, see - '.log', // Log files emitted by tools such as `tsserver`, see - '.nyc_output', // Temporary directory where nyc stores coverage data, see - '.sass-cache', // Cache folder for node-sass, see - 'bower_components', // Where Bower packages are installed, see - 'coverage', // Standard output directory for code coverage reports, see - 'node_modules', // Where Node modules are installed, see , - '**/node_modules', - '**/__tests__/**/__{helper,fixture}?(s)__/**/*', - '**/test?(s)/**/{helper,fixture}?(s)/**/*' -]; - -const hasExtension = (extensions, file) => - extensions.includes(path.extname(file).slice(1)); - -const buildExtensionPattern = extensions => - (extensions.length === 1 ? extensions[0] : `{${extensions.join(',')}}`); - -const defaultTestPatterns = (extensions) => { - const extensionPattern = buildExtensionPattern(extensions); - - return [ - `test.${extensionPattern}`, - `{src,source}/test.${extensionPattern}`, - `**/__tests__/**/*.${extensionPattern}`, - `**/*.spec.${extensionPattern}`, - `**/*.test.${extensionPattern}`, - `**/test-*.${extensionPattern}`, - `**/test/**/*.${extensionPattern}`, - `**/tests/**/*.${extensionPattern}` - ]; -}; - -const globFiles = (cwd, patterns) => { - const files = globby.sync(patterns, { - absolute: false, - braceExpansion: true, - caseSensitiveMatch: false, - cwd, - dot: false, - expandDirectories: true, - extglob: true, - followSymbolicLinks: true, - gitignore: false, - globstar: true, - ignore: defaultIgnorePatterns, - baseNameMatch: false, - onlyFiles: true, - stats: false, - unique: true - }); - - // Return absolute file paths. This has the side-effect of normalizing paths - // on Windows. - return files.map(file => path.join(cwd, file)); -}; - -const findFiles = ({ cwd, extensions, filePatterns }) => - globFiles(cwd, filePatterns).filter(file => - hasExtension(extensions, file) - ); - -const findTests = ({ cwd, extensions, filePatterns }) => - findFiles({ - cwd, - extensions, - filePatterns - }).filter(file => !path.basename(file).startsWith('_')); - -// workaround to get hidden description -// jsonValue() on errors returns {} -const extractErrorMessage = (arg) => { - // pup-firefox doesnt have this - if (arg._remoteObject) { - return arg._remoteObject.subtype === 'error' ? - arg._remoteObject.description : - undefined; - } + '.git', // Git repository files, see + '.log', // Log files emitted by tools such as `tsserver`, see + '.nyc_output', // Temporary directory where nyc stores coverage data, see + '.sass-cache', // Cache folder for node-sass, see + 'bower_components', // Where Bower packages are installed, see + 'coverage', // Standard output directory for code coverage reports, see + 'node_modules', // Where Node modules are installed, see , + '**/node_modules', + '**/__tests__/**/__{helper,fixture}?(s)__/**/*', + '**/test?(s)/**/{helper,fixture}?(s)/**/*', +] - return undefined; -}; +/** + * @param {string[]} extensions + * @param {string} file + */ +function hasExtension(extensions, file) { + return extensions.includes(path.extname(file).slice(1)) +} -const messageTypeToConsoleFn = { - log: console.log, - warning: console.warn, - error: console.error, - info: console.info, - assert: console.assert, - debug: console.debug, - trace: console.trace, - dir: console.dir, - dirxml: console.dirxml, - profile: console.profile, - profileEnd: console.profileEnd, - startGroup: console.group, - startGroupCollapsed: console.groupCollapsed, - endGroup: console.groupEnd, - table: console.table, - count: console.count, - timeEnd: console.timeEnd - - // we ignore calls to console.clear, as we don't want the page to clear our terminal - // clear: console.clear -}; -const redirectConsole = async (msg) => { - const type = msg.type(); - const consoleFn = messageTypeToConsoleFn[msg.type()]; - - if (!consoleFn) { - return; - } - const text = msg.text(); - const { url, lineNumber, columnNumber } = msg.location(); - let msgArgs; - - try { - msgArgs = await Promise.all( - msg.args().map(arg => extractErrorMessage(arg) || arg.jsonValue()) - ); - } catch (err) { - // ignore error runner was probably force stopped - } +/** + * @param {any[]} extensions + */ +function buildExtensionPattern(extensions) { + return extensions.length === 1 ? extensions[0] : `{${extensions.join(',')}}` +} - if (msgArgs && msgArgs.length > 0) { - consoleFn.apply(console, msgArgs); - } else if (text) { - let color = 'white'; +/** + * @param {string[]} extensions + */ +function defaultTestPatterns(extensions) { + const extensionPattern = buildExtensionPattern(extensions) + + return [ + `test.${extensionPattern}`, + `{src,source}/test.${extensionPattern}`, + `**/__tests__/**/*.${extensionPattern}`, + `**/*.spec.${extensionPattern}`, + `**/*.test.${extensionPattern}`, + `**/test-*.${extensionPattern}`, + `**/test/**/*.${extensionPattern}`, + `**/tests/**/*.${extensionPattern}`, + ] +} - if (text.includes('Synchronous XMLHttpRequest on the main thread is deprecated')) { - return; - } - switch (type) { - case 'error': - color = 'red'; - break; - case 'warning': - color = 'yellow'; - break; - case 'info': - case 'debug': - color = 'blue'; - break; - default: - break; - } +/** + * @param {string} cwd + * @param {string[]} patterns + */ +function globFiles(cwd, patterns) { + const files = globby.sync(patterns, { + absolute: false, + braceExpansion: true, + caseSensitiveMatch: false, + cwd, + dot: false, + expandDirectories: true, + extglob: true, + followSymbolicLinks: true, + gitignore: false, + globstar: true, + ignore: defaultIgnorePatterns, + baseNameMatch: false, + onlyFiles: true, + stats: false, + unique: true, + }) + + // Return absolute file paths. This has the side-effect of normalizing paths + // on Windows. + return files.map((file) => path.join(cwd, file)) +} - consoleFn(kleur[color](text)); - - console.info( - kleur.gray( - `${url}${ - lineNumber ? - ':' + - lineNumber + - (columnNumber ? ':' + columnNumber : '') : - '' - }` - ) - ); - } -}; +/** + * Find files + * + * @param {Object} options + * @param {string} options.cwd + * @param {string[]} options.extensions + * @param {string[]} options.filePatterns + */ +function findFiles({ cwd, extensions, filePatterns }) { + return globFiles(cwd, filePatterns).filter((file) => + hasExtension(extensions, file) + ) +} -const getPw = async (browserName) => { - const { installBrowsersWithProgressBar } = require('playwright-core/lib/install/installer'); - const setupInProcess = require('playwright-core/lib/inprocess'); - const browsers = require('playwright-core/browsers.json'); - const browsersPath = require.resolve('playwright-core/browsers.json'); +/** + * Find the tests files + * + * @param {Object} options + * @param {string} options.cwd + * @param {string[]} options.extensions + * @param {string[]} options.filePatterns + */ +function findTests({ cwd, extensions, filePatterns }) { + return findFiles({ + cwd, + extensions, + filePatterns, + }).filter((file) => !path.basename(file).startsWith('_')) +} - browsers.browsers[0].download = true; // chromium - browsers.browsers[1].download = true; // firefox - browsers.browsers[2].download = true; // webkit +/** + * workaround to get hidden description + * jsonValue() on errors returns {} + * + * @param {any} arg + */ +function extractErrorMessage(arg) { + // pup-firefox doesnt have this + if (arg._remoteObject) { + return arg._remoteObject.subtype === 'error' + ? arg._remoteObject.description + : undefined + } + + return undefined +} - fs.writeFileSync( - browsersPath, - JSON.stringify(browsers, null, 2) - ); - await installBrowsersWithProgressBar([browserName]); - const api = setupInProcess; +/** @type {Record} */ +const messageTypeToConsoleFn = { + log: console.log, + warning: console.warn, + error: console.error, + info: console.info, + assert: console.assert, + debug: console.debug, + trace: console.trace, + dir: console.dir, + dirxml: console.dirxml, + profile: console.profile, + profileEnd: console.profileEnd, + startGroup: console.group, + startGroupCollapsed: console.groupCollapsed, + endGroup: console.groupEnd, + table: console.table, + count: console.count, + timeEnd: console.timeEnd, + + // we ignore calls to console.clear, as we don't want the page to clear our terminal + // clear: console.clear +} - return api[browserName]; -}; +/** + * @param {import('playwright-core').ConsoleMessage} msg + */ +async function redirectConsole(msg) { + const type = msg.type() + const consoleFn = messageTypeToConsoleFn[type] + + if (!consoleFn) { + return + } + const text = msg.text() + const { url, lineNumber, columnNumber } = msg.location() + let msgArgs + + try { + msgArgs = await Promise.all( + msg.args().map((arg) => extractErrorMessage(arg) || arg.jsonValue()) + ) + } catch (err) { + // ignore error runner was probably force stopped + } + + if (msgArgs && msgArgs.length > 0) { + consoleFn.apply(console, msgArgs) + } else if (text) { + let color = 'white' + + if ( + text.includes( + 'Synchronous XMLHttpRequest on the main thread is deprecated' + ) + ) { + return + } + switch (type) { + case 'error': + color = 'red' + break + case 'warning': + color = 'yellow' + break + case 'info': + case 'debug': + color = 'blue' + break + default: + break + } + + // @ts-ignore + consoleFn(kleur[color](text)) + + console.info( + kleur.gray( + `${url}${ + lineNumber + ? ':' + lineNumber + (columnNumber ? ':' + columnNumber : '') + : '' + }` + ) + ) + } +} + +/** + * @template {RunnerOptions["browser"]} TBrowser + * @param {TBrowser} browserName + * @returns {Promise>>} + */ +async function getPw(browserName) { + if (!['chromium', 'firefox', 'webkit'].includes(String(browserName))) { + throw new Error(`Browser not supported: ${browserName}`) + } + const { + installBrowsersWithProgressBar, + // @ts-ignore + } = require('playwright-core/lib/install/installer') + // @ts-ignore + const setupInProcess = require('playwright-core/lib/inprocess') + const browsers = require('playwright-core/browsers.json') + const browsersPath = require.resolve('playwright-core/browsers.json') + + // @ts-ignore + browsers.browsers[0].download = true // chromium + // @ts-ignore + browsers.browsers[1].download = true // firefox + // @ts-ignore + browsers.browsers[2].download = true // webkit + + fs.writeFileSync(browsersPath, JSON.stringify(browsers, null, 2)) + await installBrowsersWithProgressBar([browserName]) + const api = setupInProcess + + return api[browserName] +} -const addWorker = filePath => ` +/** + * @param {string} filePath + */ +function addWorker(filePath) { + return ` const w = new Worker("${filePath}"); w.onmessage = function(e) { if(e.data.pwRunEnded) { self.PW_TEST.end(e.data.pwRunFailed) } } -`; - -const runnerOptions = (flags) => { - const opts = {}; - - // eslint-disable-next-line guard-for-in - for (const key in flags) { - const value = flags[key]; - const localFlags = [ - 'browser', - 'runner', - 'watch', - 'debug', - 'mode', - 'incognito', - 'extension', - 'cwd', - 'extensions', - 'assets', - 'before', - 'node', - 'cov', - 'config', - '_', - 'd', - 'r', - 'b', - 'm', - 'w', - 'i', - 'e' - ]; - - if (!localFlags.includes(key)) { - opts[camelCase(key)] = value; - } +` +} + +/** + * @param {{ [x: string]: any; }} flags + */ +function runnerOptions(flags) { + const opts = {} + + // eslint-disable-next-line guard-for-in + for (const key in flags) { + const value = flags[key] + const localFlags = [ + 'browser', + 'runner', + 'watch', + 'debug', + 'mode', + 'incognito', + 'extension', + 'cwd', + 'extensions', + 'assets', + 'before', + 'node', + 'cov', + 'config', + '_', + 'd', + 'r', + 'b', + 'm', + 'w', + 'i', + 'e', + ] + + if (!localFlags.includes(key)) { + // @ts-ignore + opts[camelCase(key)] = value } + } - return opts; -}; + return opts +} /** * Build the bundle * * @param {import("./runner")} runner - * @param {any} config - Runner esbuild config + * @param {ESBuildOptions} config - Runner esbuild config * @param {string} tmpl * @param {"bundle" | "before" | "watch"} mode */ const build = async (runner, config = {}, tmpl = '', mode = 'bundle') => { - const outName = `${mode}-out.js`; - const infile = path.join(runner.dir, 'in.js'); - const outfile = path.join(runner.dir, outName); - const sourceMapSupport = path.join(__dirname, 'vendor/source-map-support.js'); - const nodeGlobalsInject = path.join(__dirname, 'node-globals.js'); - - const nodePlugin = { - name: 'node built ins', - setup(build) { - build.onResolve({ filter: /^path$/ }, () => { - return { path: require.resolve('path-browserify') }; - }); - } - }; - - // watch mode - const watch = { - onRebuild: async (error) => { - if (!error) { - await runner.page.reload(); - runner.file = outName; - await runner.runTests(); - } - } - }; - - // main script template - let infileContent = ` + const outName = `${mode}-out.js` + const infile = path.join(runner.dir, 'in.js') + const outfile = path.join(runner.dir, outName) + const sourceMapSupport = path.join(__dirname, 'vendor/source-map-support.js') + const nodeGlobalsInject = path.join(__dirname, 'node-globals.js') + + /** @type {ESBuildPlugin} */ + const nodePlugin = { + name: 'node built ins', + setup(build) { + build.onResolve({ filter: /^path$/ }, () => { + return { path: require.resolve('path-browserify') } + }) + }, + } + + /** @type {import('esbuild').WatchMode} */ + const watch = { + onRebuild: async (error) => { + if (!error && runner.page) { + await runner.page.reload() + await runner.runTests(runner.page, outName) + } + }, + } + + // main script template + let infileContent = ` 'use strict' require('${sourceMapSupport.replace(/\\/g, '/')}').install(); process.env = ${JSON.stringify(runner.env)} ${tmpl} -${runner.tests.map(t => `require('${t.replace(/\\/g, '/')}')`).join('\n')} -`; +${runner.tests.map((t) => `require('${t.replace(/\\/g, '/')}')`).join('\n')} +` - // before script template - if (mode === 'before') { - infileContent = ` + // before script template + if (mode === 'before' && runner.options.before) { + infileContent = ` 'use strict' require('${sourceMapSupport.replace(/\\/g, '/')}').install(); process.env = ${JSON.stringify(runner.env)} require('${require.resolve('../static/setup.js').replace(/\\/g, '/')}') -require('${require.resolve(path.join(runner.options.cwd, runner.options.before)).replace(/\\/g, '/')}') -`; - } - - fs.writeFileSync(infile, infileContent); - await esbuild.build(merge( - { - entryPoints: [infile], - bundle: true, - mainFields: ['browser', 'module', 'main'], - sourcemap: 'inline', - plugins: [nodePlugin], - outfile, - inject: [nodeGlobalsInject], - watch: mode === 'watch' ? watch : false, - define: { - 'global': 'globalThis', - 'PW_TEST_SOURCEMAP': runner.options.debug ? 'false' : 'true' - } - }, - config, - runner.options.buildConfig - )); - - runner.file = outName; - - return outName; -}; +require('${require + .resolve(path.join(runner.options.cwd, runner.options.before)) + .replace(/\\/g, '/')}') +` + } + + fs.writeFileSync(infile, infileContent) + /** @type {ESBuildOptions} */ + const defaultOptions = { + entryPoints: [infile], + bundle: true, + mainFields: ['browser', 'module', 'main'], + sourcemap: 'inline', + plugins: [nodePlugin], + outfile, + inject: [nodeGlobalsInject], + watch: mode === 'watch' ? watch : false, + define: { + global: 'globalThis', + PW_TEST_SOURCEMAP: runner.options.debug ? 'false' : 'true', + }, + } + await esbuild.build(merge(defaultOptions, config, runner.options.buildConfig)) + + return outName +} /** * Create coverage report in istanbul JSON format * * @param {import("./runner")} runner * @param {any} coverage + * @param {string} file */ -const createCov = async (runner, coverage) => { - const spinner = ora('Generating code coverage.').start(); - const entries = {}; - const { cwd } = runner.options; - const TestExclude = require('test-exclude'); - const exclude = new TestExclude(); - const f = exclude.globSync().map(f => path.resolve(f)); - - for (const entry of coverage) { - const filePath = path.join(runner.dir, entry.url.replace(runner.url, '')); - - if (filePath.includes(runner.file)) { - const converter = new V8ToIstanbul(filePath, 0, { source: entry.source }); - - // eslint-disable-next-line no-await-in-loop - await converter.load(); - converter.applyCoverage(entry.functions); - const instanbul = converter.toIstanbul(); - - // eslint-disable-next-line guard-for-in - for (const key in instanbul) { - if (f.includes(key)) { - entries[key] = instanbul[key]; - } - } +const createCov = async (runner, coverage, file) => { + const spinner = ora('Generating code coverage.').start() + const entries = {} + const { cwd } = runner.options + // @ts-ignore + const TestExclude = require('test-exclude') + const exclude = new TestExclude() + // @ts-ignore + const f = exclude.globSync().map((f) => path.resolve(f)) + + for (const entry of coverage) { + const filePath = path.join(runner.dir, entry.url.replace(runner.url, '')) + + if (filePath.includes(file)) { + // @ts-ignore + const converter = new V8ToIstanbul(filePath, 0, { source: entry.source }) + + // eslint-disable-next-line no-await-in-loop + await converter.load() + converter.applyCoverage(entry.functions) + const instanbul = converter.toIstanbul() + + // eslint-disable-next-line guard-for-in + for (const key in instanbul) { + if (f.includes(key)) { + // @ts-ignore + entries[key] = instanbul[key] } + } } - const covPath = path.join(cwd, '.nyc_output'); + } + const covPath = path.join(cwd, '.nyc_output') - await mkdir(covPath, { recursive: true }); + await mkdir(covPath, { recursive: true }) - await writeFile(path.join(covPath, 'coverage-pw.json'), JSON.stringify(entries)); - spinner.succeed('Code coverage generated, run "npx nyc report".'); -}; + await writeFile( + path.join(covPath, 'coverage-pw.json'), + JSON.stringify(entries) + ) + spinner.succeed('Code coverage generated, run "npx nyc report".') +} /** * Get a free port @@ -388,72 +463,80 @@ const createCov = async (runner, coverage) => { * @returns {Promise} */ function getPort(port = 3000, host = '127.0.0.1') { - const server = createServer(); - - return new Promise((resolve, reject) => { - server.on('error', (err) => { - // @ts-ignore - if (err.code === 'EADDRINUSE' || err.code === 'EACCES') { - server.listen(0, host); - } else { - reject(err); - } - }); - server.on('listening', () => { - // @ts-ignore - const { port } = server.address(); - - server.close(() => resolve(port)); - }); - server.listen(port, host); - }); + const server = createServer() + + return new Promise((resolve, reject) => { + server.on('error', (err) => { + // @ts-ignore + if (err.code === 'EADDRINUSE' || err.code === 'EACCES') { + server.listen(0, host) + } else { + reject(err) + } + }) + server.on('listening', () => { + // @ts-ignore + const { port } = server.address() + + server.close(() => resolve(port)) + }) + server.listen(port, host) + }) } -const createPolka = runner => new Promise(async (resolve, reject) => { - const host = '127.0.0.1'; - const port = await getPort(3000, host); - const url = `http://${host}:${port}/`; - +/** + * @param {import('./runner')} runner + */ +async function createPolka(runner) { + const host = '127.0.0.1' + const port = await getPort(3000, host) + const url = `http://${host}:${port}/` + return new Promise((resolve, reject) => { const { server } = polka() - .use( - sirv(runner.dir, { - dev: true, - setHeaders: (rsp, pathname) => { - if (pathname === '/') { - rsp.setHeader( - 'Clear-Site-Data', - '"cache", "cookies", "storage"' - ); - // rsp.setHeader('Clear-Site-Data', '"cache", "cookies", "storage", "executionContexts"'); - } - } - }) - ) - .use( - sirv(path.join(runner.options.cwd, runner.options.assets), { dev: true }) - ) - .listen(port, host, (err) => { - if (err) { - reject(err); - - return; + .use( + // @ts-ignore + sirv(runner.dir, { + dev: true, + setHeaders: ( + /** @type {{ setHeader: (arg0: string, arg1: string) => void; }} */ rsp, + /** @type {string} */ pathname + ) => { + if (pathname === '/') { + rsp.setHeader('Clear-Site-Data', '"cache", "cookies", "storage"') + // rsp.setHeader('Clear-Site-Data', '"cache", "cookies", "storage", "executionContexts"'); } - runner.url = url; - runner.server = server; - resolve(); - }); -}); + }, + }) + ) + .use( + // @ts-ignore + sirv(path.join(runner.options.cwd, runner.options.assets), { + dev: true, + }) + ) + .listen(port, host, (/** @type {Error} */ err) => { + if (err) { + reject(err) + + return + } + runner.url = url + runner.server = server + resolve(true) + }) + }) +} module.exports = { - extractErrorMessage, - redirectConsole, - defaultTestPatterns, - findTests, - findFiles, - getPw, - addWorker, - runnerOptions, - build, - createCov, - createPolka -}; + extractErrorMessage, + redirectConsole, + defaultTestPatterns, + findTests, + findFiles, + getPw, + addWorker, + runnerOptions, + build, + createCov, + createPolka, +} diff --git a/src/vendor/benchmark.js b/src/vendor/benchmark.js index 6ecab5d8..0f59b3f0 100644 --- a/src/vendor/benchmark.js +++ b/src/vendor/benchmark.js @@ -1,3 +1,4 @@ +// @ts-nocheck /* ! * Benchmark.js * Copyright 2010-2016 Mathias Bynens @@ -5,2914 +6,3354 @@ * Modified by John-David Dalton * Available under MIT license */ -(function() { - 'use strict'; - - /** Used as a safe reference for `undefined` in pre ES5 environments. */ - let undefined; - - /** Used to determine if values are of the language type Object. */ - const objectTypes = { - 'function': true, - 'object': true - }; - - /** Used as a reference to the global object. */ - let root = globalThis; // (objectTypes[typeof window] && window) || this; - - /** Detect free variable `define`. */ - const freeDefine = typeof define == 'function' && typeof define.amd == 'object' && define.amd && define; - - /** Detect free variable `exports`. */ - const freeExports = objectTypes[typeof exports] && exports && !exports.nodeType && exports; - - /** Detect free variable `module`. */ - const freeModule = objectTypes[typeof module] && module && !module.nodeType && module; - - /** Detect free variable `global` from Node.js or Browserified code and use it as `root`. */ - const freeGlobal = freeExports && freeModule && typeof global == 'object' && global; - - if (freeGlobal && (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal || freeGlobal.self === freeGlobal)) { - root = freeGlobal; +;(function () { + 'use strict' + + /** Used as a safe reference for `undefined` in pre ES5 environments. */ + let undefined + + /** Used to determine if values are of the language type Object. */ + const objectTypes = { + function: true, + object: true, + } + + /** Used as a reference to the global object. */ + let root = globalThis // (objectTypes[typeof window] && window) || this; + + /** Detect free variable `define`. */ + const freeDefine = + typeof define == 'function' && + typeof define.amd == 'object' && + define.amd && + define + + /** Detect free variable `exports`. */ + const freeExports = + objectTypes[typeof exports] && exports && !exports.nodeType && exports + + /** Detect free variable `module`. */ + const freeModule = + objectTypes[typeof module] && module && !module.nodeType && module + + /** Detect free variable `global` from Node.js or Browserified code and use it as `root`. */ + const freeGlobal = + freeExports && freeModule && typeof global == 'object' && global + + if ( + freeGlobal && + (freeGlobal.global === freeGlobal || + freeGlobal.window === freeGlobal || + freeGlobal.self === freeGlobal) + ) { + root = freeGlobal + } + + /** Detect free variable `require`. */ + const freeRequire = typeof require == 'function' && require + + /** Used to assign each benchmark an incremented id. */ + let counter = 0 + + /** Detect the popular CommonJS extension `module.exports`. */ + const moduleExports = + freeModule && freeModule.exports === freeExports && freeExports + + /** Used to detect primitive types. */ + const rePrimitive = /^(?:boolean|number|string|undefined)$/ + + /** Used to make every compiled test unique. */ + let uidCounter = 0 + + /** Used to assign default `context` object properties. */ + const contextProps = [ + 'Array', + 'Date', + 'Function', + 'Math', + 'Object', + 'RegExp', + 'String', + '_', + 'clearTimeout', + 'chrome', + 'chromium', + 'document', + 'navigator', + 'phantom', + 'platform', + 'process', + 'runtime', + 'setTimeout', + ] + + /** Used to avoid hz of Infinity. */ + const divisors = { + 1: 4096, + 2: 512, + 3: 64, + 4: 8, + 5: 0, + } + + /** + * T-Distribution two-tailed critical values for 95% confidence. + * For more info see http://www.itl.nist.gov/div898/handbook/eda/section3/eda3672.htm. + */ + const tTable = { + 1: 12.706, + 2: 4.303, + 3: 3.182, + 4: 2.776, + 5: 2.571, + 6: 2.447, + 7: 2.365, + 8: 2.306, + 9: 2.262, + 10: 2.228, + 11: 2.201, + 12: 2.179, + 13: 2.16, + 14: 2.145, + 15: 2.131, + 16: 2.12, + 17: 2.11, + 18: 2.101, + 19: 2.093, + 20: 2.086, + 21: 2.08, + 22: 2.074, + 23: 2.069, + 24: 2.064, + 25: 2.06, + 26: 2.056, + 27: 2.052, + 28: 2.048, + 29: 2.045, + 30: 2.042, + infinity: 1.96, + } + + /** + * Critical Mann-Whitney U-values for 95% confidence. + * For more info see http://www.saburchill.com/IBbiology/stats/003.html. + */ + const uTable = { + 5: [0, 1, 2], + 6: [1, 2, 3, 5], + 7: [1, 3, 5, 6, 8], + 8: [2, 4, 6, 8, 10, 13], + 9: [2, 4, 7, 10, 12, 15, 17], + 10: [3, 5, 8, 11, 14, 17, 20, 23], + 11: [3, 6, 9, 13, 16, 19, 23, 26, 30], + 12: [4, 7, 11, 14, 18, 22, 26, 29, 33, 37], + 13: [4, 8, 12, 16, 20, 24, 28, 33, 37, 41, 45], + 14: [5, 9, 13, 17, 22, 26, 31, 36, 40, 45, 50, 55], + 15: [5, 10, 14, 19, 24, 29, 34, 39, 44, 49, 54, 59, 64], + 16: [6, 11, 15, 21, 26, 31, 37, 42, 47, 53, 59, 64, 70, 75], + 17: [6, 11, 17, 22, 28, 34, 39, 45, 51, 57, 63, 67, 75, 81, 87], + 18: [7, 12, 18, 24, 30, 36, 42, 48, 55, 61, 67, 74, 80, 86, 93, 99], + 19: [7, 13, 19, 25, 32, 38, 45, 52, 58, 65, 72, 78, 85, 92, 99, 106, 113], + 20: [ + 8, + 14, + 20, + 27, + 34, + 41, + 48, + 55, + 62, + 69, + 76, + 83, + 90, + 98, + 105, + 112, + 119, + 127, + ], + 21: [ + 8, + 15, + 22, + 29, + 36, + 43, + 50, + 58, + 65, + 73, + 80, + 88, + 96, + 103, + 111, + 119, + 126, + 134, + 142, + ], + 22: [ + 9, + 16, + 23, + 30, + 38, + 45, + 53, + 61, + 69, + 77, + 85, + 93, + 101, + 109, + 117, + 125, + 133, + 141, + 150, + 158, + ], + 23: [ + 9, + 17, + 24, + 32, + 40, + 48, + 56, + 64, + 73, + 81, + 89, + 98, + 106, + 115, + 123, + 132, + 140, + 149, + 157, + 166, + 175, + ], + 24: [ + 10, + 17, + 25, + 33, + 42, + 50, + 59, + 67, + 76, + 85, + 94, + 102, + 111, + 120, + 129, + 138, + 147, + 156, + 165, + 174, + 183, + 192, + ], + 25: [ + 10, + 18, + 27, + 35, + 44, + 53, + 62, + 71, + 80, + 89, + 98, + 107, + 117, + 126, + 135, + 145, + 154, + 163, + 173, + 182, + 192, + 201, + 211, + ], + 26: [ + 11, + 19, + 28, + 37, + 46, + 55, + 64, + 74, + 83, + 93, + 102, + 112, + 122, + 132, + 141, + 151, + 161, + 171, + 181, + 191, + 200, + 210, + 220, + 230, + ], + 27: [ + 11, + 20, + 29, + 38, + 48, + 57, + 67, + 77, + 87, + 97, + 107, + 118, + 125, + 138, + 147, + 158, + 168, + 178, + 188, + 199, + 209, + 219, + 230, + 240, + 250, + ], + 28: [ + 12, + 21, + 30, + 40, + 50, + 60, + 70, + 80, + 90, + 101, + 111, + 122, + 132, + 143, + 154, + 164, + 175, + 186, + 196, + 207, + 218, + 228, + 239, + 250, + 261, + 272, + ], + 29: [ + 13, + 22, + 32, + 42, + 52, + 62, + 73, + 83, + 94, + 105, + 116, + 127, + 138, + 149, + 160, + 171, + 182, + 193, + 204, + 215, + 226, + 238, + 249, + 260, + 271, + 282, + 294, + ], + 30: [ + 13, + 23, + 33, + 43, + 54, + 65, + 76, + 87, + 98, + 109, + 120, + 131, + 143, + 154, + 166, + 177, + 189, + 200, + 212, + 223, + 235, + 247, + 258, + 270, + 282, + 293, + 305, + 317, + ], + } + + /* --------------------------------------------------------------------------*/ + + /** + * Create a new `Benchmark` function using the given `context` object. + * + * @static + * @memberOf Benchmark + * @param {Object} [context=root] The context object. + * @returns {Function} Returns a new `Benchmark` function. + */ + function runInContext(context) { + // Exit early if unable to acquire lodash. + const _ = (context && context._) || require('lodash') || root._ + + if (!_) { + Benchmark.runInContext = runInContext + + return Benchmark } - - /** Detect free variable `require`. */ - const freeRequire = typeof require == 'function' && require; - - /** Used to assign each benchmark an incremented id. */ - let counter = 0; - - /** Detect the popular CommonJS extension `module.exports`. */ - const moduleExports = freeModule && freeModule.exports === freeExports && freeExports; - - /** Used to detect primitive types. */ - const rePrimitive = /^(?:boolean|number|string|undefined)$/; - - /** Used to make every compiled test unique. */ - let uidCounter = 0; - - /** Used to assign default `context` object properties. */ - const contextProps = [ - 'Array', 'Date', 'Function', 'Math', 'Object', 'RegExp', 'String', '_', - 'clearTimeout', 'chrome', 'chromium', 'document', 'navigator', 'phantom', - 'platform', 'process', 'runtime', 'setTimeout' - ]; - - /** Used to avoid hz of Infinity. */ - const divisors = { - '1': 4096, - '2': 512, - '3': 64, - '4': 8, - '5': 0 - }; - - /** - * T-Distribution two-tailed critical values for 95% confidence. - * For more info see http://www.itl.nist.gov/div898/handbook/eda/section3/eda3672.htm. - */ - const tTable = { - '1': 12.706, - '2': 4.303, - '3': 3.182, - '4': 2.776, - '5': 2.571, - '6': 2.447, - '7': 2.365, - '8': 2.306, - '9': 2.262, - '10': 2.228, - '11': 2.201, - '12': 2.179, - '13': 2.16, - '14': 2.145, - '15': 2.131, - '16': 2.12, - '17': 2.11, - '18': 2.101, - '19': 2.093, - '20': 2.086, - '21': 2.08, - '22': 2.074, - '23': 2.069, - '24': 2.064, - '25': 2.06, - '26': 2.056, - '27': 2.052, - '28': 2.048, - '29': 2.045, - '30': 2.042, - 'infinity': 1.96 - }; - - /** - * Critical Mann-Whitney U-values for 95% confidence. - * For more info see http://www.saburchill.com/IBbiology/stats/003.html. - */ - const uTable = { - '5': [0, 1, 2], - '6': [1, 2, 3, 5], - '7': [1, 3, 5, 6, 8], - '8': [2, 4, 6, 8, 10, 13], - '9': [2, 4, 7, 10, 12, 15, 17], - '10': [3, 5, 8, 11, 14, 17, 20, 23], - '11': [3, 6, 9, 13, 16, 19, 23, 26, 30], - '12': [4, 7, 11, 14, 18, 22, 26, 29, 33, 37], - '13': [4, 8, 12, 16, 20, 24, 28, 33, 37, 41, 45], - '14': [5, 9, 13, 17, 22, 26, 31, 36, 40, 45, 50, 55], - '15': [5, 10, 14, 19, 24, 29, 34, 39, 44, 49, 54, 59, 64], - '16': [6, 11, 15, 21, 26, 31, 37, 42, 47, 53, 59, 64, 70, 75], - '17': [6, 11, 17, 22, 28, 34, 39, 45, 51, 57, 63, 67, 75, 81, 87], - '18': [7, 12, 18, 24, 30, 36, 42, 48, 55, 61, 67, 74, 80, 86, 93, 99], - '19': [7, 13, 19, 25, 32, 38, 45, 52, 58, 65, 72, 78, 85, 92, 99, 106, 113], - '20': [8, 14, 20, 27, 34, 41, 48, 55, 62, 69, 76, 83, 90, 98, 105, 112, 119, 127], - '21': [8, 15, 22, 29, 36, 43, 50, 58, 65, 73, 80, 88, 96, 103, 111, 119, 126, 134, 142], - '22': [9, 16, 23, 30, 38, 45, 53, 61, 69, 77, 85, 93, 101, 109, 117, 125, 133, 141, 150, 158], - '23': [9, 17, 24, 32, 40, 48, 56, 64, 73, 81, 89, 98, 106, 115, 123, 132, 140, 149, 157, 166, 175], - '24': [10, 17, 25, 33, 42, 50, 59, 67, 76, 85, 94, 102, 111, 120, 129, 138, 147, 156, 165, 174, 183, 192], - '25': [10, 18, 27, 35, 44, 53, 62, 71, 80, 89, 98, 107, 117, 126, 135, 145, 154, 163, 173, 182, 192, 201, 211], - '26': [11, 19, 28, 37, 46, 55, 64, 74, 83, 93, 102, 112, 122, 132, 141, 151, 161, 171, 181, 191, 200, 210, 220, 230], - '27': [11, 20, 29, 38, 48, 57, 67, 77, 87, 97, 107, 118, 125, 138, 147, 158, 168, 178, 188, 199, 209, 219, 230, 240, 250], - '28': [12, 21, 30, 40, 50, 60, 70, 80, 90, 101, 111, 122, 132, 143, 154, 164, 175, 186, 196, 207, 218, 228, 239, 250, 261, 272], - '29': [13, 22, 32, 42, 52, 62, 73, 83, 94, 105, 116, 127, 138, 149, 160, 171, 182, 193, 204, 215, 226, 238, 249, 260, 271, 282, 294], - '30': [13, 23, 33, 43, 54, 65, 76, 87, 98, 109, 120, 131, 143, 154, 166, 177, 189, 200, 212, 223, 235, 247, 258, 270, 282, 293, 305, 317] - }; - - /* --------------------------------------------------------------------------*/ + // Avoid issues with some ES3 environments that attempt to use values, named + // after built-in constructors like `Object`, for the creation of literals. + // ES5 clears this up by stating that literals must use built-in constructors. + // See http://es5.github.io/#x11.1.5. + context = context + ? _.defaults(root.Object(), context, _.pick(root, contextProps)) + : root + + /** Native constructor references. */ + let Array = context.Array, + Date = context.Date, + Function = context.Function, + Math = context.Math, + Object = context.Object, + RegExp = context.RegExp, + String = context.String + + /** Used for `Array` and `Object` method references. */ + let arrayRef = [], + objectProto = Object.prototype + + /** Native method shortcuts. */ + let abs = Math.abs, + clearTimeout = context.clearTimeout, + floor = Math.floor, + log = Math.log, + max = Math.max, + min = Math.min, + pow = Math.pow, + push = arrayRef.push, + setTimeout = context.setTimeout, + shift = arrayRef.shift, + slice = arrayRef.slice, + sqrt = Math.sqrt, + toString = objectProto.toString, + unshift = arrayRef.unshift + + /** Used to avoid inclusion in Browserified bundles. */ + const req = require + + /** Detect DOM document object. */ + const doc = isHostType(context, 'document') && context.document + + /** Used to access Wade Simmons' Node.js `microtime` module. */ + const microtimeObject = req('microtime') + + /** Used to access Node.js's high resolution timer. */ + const processObject = isHostType(context, 'process') && context.process + + /** Used to prevent a `removeChild` memory leak in IE < 9. */ + const trash = doc && doc.createElement('div') + + /** Used to integrity check compiled tests. */ + const uid = 'uid' + _.now() + + /** Used to avoid infinite recursion when methods call each other. */ + const calledBy = {} /** - * Create a new `Benchmark` function using the given `context` object. + * An object used to flag environments/features. * * @static * @memberOf Benchmark - * @param {Object} [context=root] The context object. - * @returns {Function} Returns a new `Benchmark` function. + * @type Object */ - function runInContext(context) { - // Exit early if unable to acquire lodash. - const _ = context && context._ || require('lodash') || root._; - - if (!_) { - Benchmark.runInContext = runInContext; - - return Benchmark; - } - // Avoid issues with some ES3 environments that attempt to use values, named - // after built-in constructors like `Object`, for the creation of literals. - // ES5 clears this up by stating that literals must use built-in constructors. - // See http://es5.github.io/#x11.1.5. - context = context ? _.defaults(root.Object(), context, _.pick(root, contextProps)) : root; - - /** Native constructor references. */ - let Array = context.Array, - Date = context.Date, - Function = context.Function, - Math = context.Math, - Object = context.Object, - RegExp = context.RegExp, - String = context.String; - - /** Used for `Array` and `Object` method references. */ - let arrayRef = [], - objectProto = Object.prototype; - - /** Native method shortcuts. */ - let abs = Math.abs, - clearTimeout = context.clearTimeout, - floor = Math.floor, - log = Math.log, - max = Math.max, - min = Math.min, - pow = Math.pow, - push = arrayRef.push, - setTimeout = context.setTimeout, - shift = arrayRef.shift, - slice = arrayRef.slice, - sqrt = Math.sqrt, - toString = objectProto.toString, - unshift = arrayRef.unshift; - - /** Used to avoid inclusion in Browserified bundles. */ - const req = require; - - /** Detect DOM document object. */ - const doc = isHostType(context, 'document') && context.document; - - /** Used to access Wade Simmons' Node.js `microtime` module. */ - const microtimeObject = req('microtime'); - - /** Used to access Node.js's high resolution timer. */ - const processObject = isHostType(context, 'process') && context.process; - - /** Used to prevent a `removeChild` memory leak in IE < 9. */ - const trash = doc && doc.createElement('div'); - - /** Used to integrity check compiled tests. */ - const uid = 'uid' + _.now(); - - /** Used to avoid infinite recursion when methods call each other. */ - const calledBy = {}; - - /** - * An object used to flag environments/features. - * - * @static - * @memberOf Benchmark - * @type Object - */ - const support = {}; - - (function() { - /** - * Detect if running in a browser environment. - * - * @memberOf Benchmark.support - * @type boolean - */ - support.browser = doc && isHostType(context, 'navigator') && !isHostType(context, 'phantom'); - - /** - * Detect if the Timers API exists. - * - * @memberOf Benchmark.support - * @type boolean - */ - support.timeout = isHostType(context, 'setTimeout') && isHostType(context, 'clearTimeout'); - - /** - * Detect if function decompilation is support. - * - * @name decompilation - * @memberOf Benchmark.support - * @type boolean - */ - try { - // Safari 2.x removes commas in object literals from `Function#toString` results. - // See http://webk.it/11609 for more details. - // Firefox 3.6 and Opera 9.25 strip grouping parentheses from `Function#toString` results. - // See http://bugzil.la/559438 for more details. - support.decompilation = Function( - ('return (' + (function(x) { - return { - 'x': String(String(1 + x)), - 'y': 0 - }; - }) + ')') - // Avoid issues with code added by Istanbul. - .replace(/__cov__[^;]+;/g, '') - )()(0).x === '1'; - } catch (e) { - support.decompilation = false; - } - })(); - - /** - * Timer object used by `clock()` and `Deferred#resolve`. - * - * @private - * @type Object - */ - let timer = { - - /** - * The timer namespace object or constructor. - * - * @private - * @memberOf timer - * @type {Function|Object} - */ - 'ns': Date, - - /** - * Starts the deferred timer. - * - * @private - * @memberOf timer - * @param {Object} deferred The deferred instance. - */ - 'start': null, // Lazy defined in `clock()`. - - /** - * Stops the deferred timer. - * - * @private - * @memberOf timer - * @param {Object} deferred The deferred instance. - */ - 'stop': null // Lazy defined in `clock()`. - }; - - /* ------------------------------------------------------------------------*/ - - /** - * The Benchmark constructor. - * - * Note: The Benchmark constructor exposes a handful of lodash methods to - * make working with arrays, collections, and objects easier. The lodash - * methods are: - * [`each/forEach`](https://lodash.com/docs#forEach), [`forOwn`](https://lodash.com/docs#forOwn), - * [`has`](https://lodash.com/docs#has), [`indexOf`](https://lodash.com/docs#indexOf), - * [`map`](https://lodash.com/docs#map), and [`reduce`](https://lodash.com/docs#reduce) - * - * @constructor - * @param {string} name A name to identify the benchmark. - * @param {Function|string} fn The test to benchmark. - * @param {Object} [options={}] Options object. - * @example - * - * // basic usage (the `new` operator is optional) - * var bench = new Benchmark(fn); - * - * // or using a name first - * var bench = new Benchmark('foo', fn); - * - * // or with options - * var bench = new Benchmark('foo', fn, { - * - * // displayed by `Benchmark#toString` if `name` is not available - * 'id': 'xyz', - * - * // called when the benchmark starts running - * 'onStart': onStart, - * - * // called after each run cycle - * 'onCycle': onCycle, - * - * // called when aborted - * 'onAbort': onAbort, - * - * // called when a test errors - * 'onError': onError, - * - * // called when reset - * 'onReset': onReset, - * - * // called when the benchmark completes running - * 'onComplete': onComplete, - * - * // compiled/called before the test loop - * 'setup': setup, - * - * // compiled/called after the test loop - * 'teardown': teardown - * }); - * - * // or name and options - * var bench = new Benchmark('foo', { - * - * // a flag to indicate the benchmark is deferred - * 'defer': true, - * - * // benchmark test function - * 'fn': function(deferred) { - * // call `Deferred#resolve` when the deferred test is finished - * deferred.resolve(); - * } - * }); - * - * // or options only - * var bench = new Benchmark({ - * - * // benchmark name - * 'name': 'foo', - * - * // benchmark test as a string - * 'fn': '[1,2,3,4].sort()' - * }); - * - * // a test's `this` binding is set to the benchmark instance - * var bench = new Benchmark('foo', function() { - * 'My name is '.concat(this.name); // "My name is foo" - * }); - */ - function Benchmark(name, fn, options) { - const bench = this; - - // Allow instance creation without the `new` operator. - if (!(bench instanceof Benchmark)) { - return new Benchmark(name, fn, options); - } - // Juggle arguments. - if (_.isPlainObject(name)) { - // 1 argument (options). - options = name; - } else if (_.isFunction(name)) { - // 2 arguments (fn, options). - options = fn; - fn = name; - } else if (_.isPlainObject(fn)) { - // 2 arguments (name, options). - options = fn; - fn = null; - bench.name = name; - } else { - // 3 arguments (name, fn [, options]). - bench.name = name; - } - setOptions(bench, options); - - bench.id || (bench.id = ++counter); - bench.fn == null && (bench.fn = fn); - - bench.stats = cloneDeep(bench.stats); - bench.times = cloneDeep(bench.times); - } - - /** - * The Deferred constructor. - * - * @constructor - * @memberOf Benchmark - * @param {Object} clone The cloned benchmark instance. - */ - function Deferred(clone) { - const deferred = this; - - if (!(deferred instanceof Deferred)) { - return new Deferred(clone); - } - deferred.benchmark = clone; - clock(deferred); - } + const support = {} - /** - * The Event constructor. + ;(function () { + /** + * Detect if running in a browser environment. * - * @constructor - * @memberOf Benchmark - * @param {Object|string} type The event type. + * @memberOf Benchmark.support + * @type boolean */ - function Event(type) { - const event = this; - - if (type instanceof Event) { - return type; - } - - return (event instanceof Event) ? - _.assign(event, { 'timeStamp': _.now() }, typeof type == 'string' ? { 'type': type } : type) : - new Event(type); - } + support.browser = + doc && + isHostType(context, 'navigator') && + !isHostType(context, 'phantom') - /** - * The Suite constructor. - * - * Note: Each Suite instance has a handful of wrapped lodash methods to - * make working with Suites easier. The wrapped lodash methods are: - * [`each/forEach`](https://lodash.com/docs#forEach), [`indexOf`](https://lodash.com/docs#indexOf), - * [`map`](https://lodash.com/docs#map), and [`reduce`](https://lodash.com/docs#reduce) - * - * @constructor - * @memberOf Benchmark - * @param {string} name A name to identify the suite. - * @param {Object} [options={}] Options object. - * @example - * - * // basic usage (the `new` operator is optional) - * var suite = new Benchmark.Suite; + /** + * Detect if the Timers API exists. * - * // or using a name first - * var suite = new Benchmark.Suite('foo'); - * - * // or with options - * var suite = new Benchmark.Suite('foo', { - * - * // called when the suite starts running - * 'onStart': onStart, - * - * // called between running benchmarks - * 'onCycle': onCycle, - * - * // called when aborted - * 'onAbort': onAbort, - * - * // called when a test errors - * 'onError': onError, - * - * // called when reset - * 'onReset': onReset, - * - * // called when the suite completes running - * 'onComplete': onComplete - * }); + * @memberOf Benchmark.support + * @type boolean */ - function Suite(name, options) { - const suite = this; - - // Allow instance creation without the `new` operator. - if (!(suite instanceof Suite)) { - return new Suite(name, options); - } - // Juggle arguments. - if (_.isPlainObject(name)) { - // 1 argument (options). - options = name; - } else { - // 2 arguments (name [, options]). - suite.name = name; - } - setOptions(suite, options); - } + support.timeout = + isHostType(context, 'setTimeout') && isHostType(context, 'clearTimeout') - /* ------------------------------------------------------------------------*/ - - /** - * A specialized version of `_.cloneDeep` which only clones arrays and plain - * objects assigning all other values by reference. - * - * @private - * @param {*} value The value to clone. - * @returns {*} The cloned value. - */ - var cloneDeep = _.partial(_.cloneDeepWith, _, (value) => { - // Only clone primitives, arrays, and plain objects. - if (!_.isArray(value) && !_.isPlainObject(value)) { - return value; - } - }); - - /** - * Creates a function from the given arguments string and body. - * - * @private - * @param {string} args The comma separated function arguments. - * @param {string} body The function body. - * @returns {Function} The new function. - */ - function createFunction() { - // Lazy define. - createFunction = function(args, body) { - let result, - anchor = freeDefine ? freeDefine.amd : Benchmark, - prop = uid + 'createFunction'; - - runScript((freeDefine ? 'define.amd.' : 'Benchmark.') + prop + '=function(' + args + '){' + body + '}'); - result = anchor[prop]; - delete anchor[prop]; - - return result; - }; - // Fix JaegerMonkey bug. - // For more information see http://bugzil.la/639720. - createFunction = support.browser && (createFunction('', 'return"' + uid + '"') || _.noop)() == uid ? createFunction : Function; - - return createFunction.apply(null, arguments); - } - - /** - * Delay the execution of a function based on the benchmark's `delay` property. + /** + * Detect if function decompilation is support. * - * @private - * @param {Object} bench The benchmark instance. - * @param {Object} fn The function to execute. + * @name decompilation + * @memberOf Benchmark.support + * @type boolean */ - function delay(bench, fn) { - bench._timerId = _.delay(fn, bench.delay * 1e3); - } - - /** - * Destroys the given element. - * - * @private - * @param {Element} element The element to destroy. - */ - function destroyElement(element) { - trash.appendChild(element); - trash.innerHTML = ''; - } - - /** - * Gets the name of the first argument from a function's source. - * - * @private - * @param {Function} fn The function. - * @returns {string} The argument name. - */ - function getFirstArgument(fn) { - return (!_.has(fn, 'toString') && - (/^[\s(]*function[^(]*\(([^\s,)]+)/.exec(fn) || 0)[1]) || ''; - } - - /** - * Computes the arithmetic mean of a sample. - * - * @private - * @param {Array} sample The sample. - * @returns {number} The mean. - */ - function getMean(sample) { - return (_.reduce(sample, (sum, x) => sum + x) / sample.length) || 0; - } - - /** - * Gets the source code of a function. - * - * @private - * @param {Function} fn The function. - * @returns {string} The function's source code. - */ - function getSource(fn) { - let result = ''; - - if (isStringable(fn)) { - result = String(fn); - } else if (support.decompilation) { - // Escape the `{` for Firefox 1. - result = _.result(/^[^{]+\{([\s\S]*)\}\s*$/.exec(fn), 1); - } - // Trim string. - result = (result || '').replace(/^\s+|\s+$/g, ''); - - // Detect strings containing only the "use strict" directive. - return /^(?:\/\*+[\w\W]*?\*\/|\/\/.*?[\n\r\u2028\u2029]|\s)*(["'])use strict\1;?$/.test(result) ? - '' : - result; - } - - /** - * Checks if an object is of the specified class. - * - * @private - * @param {*} value The value to check. - * @param {string} name The name of the class. - * @returns {boolean} Returns `true` if the value is of the specified class, else `false`. - */ - function isClassOf(value, name) { - return value != null && toString.call(value) == '[object ' + name + ']'; - } - - /** - * Host objects can return type values that are different from their actual - * data type. The objects we are concerned with usually return non-primitive - * types of "object", "function", or "unknown". - * - * @private - * @param {*} object The owner of the property. - * @param {string} property The property to check. - * @returns {boolean} Returns `true` if the property value is a non-primitive, else `false`. - */ - function isHostType(object, property) { - if (object == null) { - return false; - } - const type = typeof object[property]; - - return !rePrimitive.test(type) && (type != 'object' || Boolean(object[property])); - } - - /** - * Checks if a value can be safely coerced to a string. - * - * @private - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if the value can be coerced, else `false`. - */ - function isStringable(value) { - return _.isString(value) || (_.has(value, 'toString') && _.isFunction(value.toString)); - } + try { + // Safari 2.x removes commas in object literals from `Function#toString` results. + // See http://webk.it/11609 for more details. + // Firefox 3.6 and Opera 9.25 strip grouping parentheses from `Function#toString` results. + // See http://bugzil.la/559438 for more details. + support.decompilation = + Function( + ( + 'return (' + + function (x) { + return { + x: String(String(1 + x)), + y: 0, + } + } + + ')' + ) + // Avoid issues with code added by Istanbul. + .replace(/__cov__[^;]+;/g, '') + )()(0).x === '1' + } catch (e) { + support.decompilation = false + } + })() - /** - * A wrapper around `require` to suppress `module missing` errors. + /** + * Timer object used by `clock()` and `Deferred#resolve`. + * + * @private + * @type Object + */ + let timer = { + /** + * The timer namespace object or constructor. * * @private - * @param {string} id The module id. - * @returns {*} The exported module or `null`. + * @memberOf timer + * @type {Function|Object} */ - function require(id) { - try { - var result = freeExports && freeRequire(id); - } catch (e) {} - - return result || null; - } + ns: Date, - /** - * Runs a snippet of JavaScript via script injection. + /** + * Starts the deferred timer. * * @private - * @param {string} code The code to run. + * @memberOf timer + * @param {Object} deferred The deferred instance. */ - function runScript(code) { - let anchor = freeDefine ? define.amd : Benchmark, - script = doc.createElement('script'), - sibling = doc.getElementsByTagName('script')[0], - parent = sibling.parentNode, - prop = uid + 'runScript', - prefix = '(' + (freeDefine ? 'define.amd.' : 'Benchmark.') + prop + '||function(){})();'; - - // Firefox 2.0.0.2 cannot use script injection as intended because it executes - // asynchronously, but that's OK because script injection is only used to avoid - // the previously commented JaegerMonkey bug. - try { - // Remove the inserted script *before* running the code to avoid differences - // in the expected script element count/order of the document. - script.appendChild(doc.createTextNode(prefix + code)); - anchor[prop] = function() { - destroyElement(script); - }; - } catch (e) { - parent = parent.cloneNode(false); - sibling = null; - script.text = code; - } - parent.insertBefore(script, sibling); - delete anchor[prop]; - } + start: null, // Lazy defined in `clock()`. - /** - * A helper function for setting options/event handlers. + /** + * Stops the deferred timer. * * @private - * @param {Object} object The benchmark or suite instance. - * @param {Object} [options={}] Options object. - */ - function setOptions(object, options) { - options = object.options = _.assign({}, cloneDeep(object.constructor.options), cloneDeep(options)); - - _.forOwn(options, (value, key) => { - if (value != null) { - // Add event listeners. - if (/^on[A-Z]/.test(key)) { - _.each(key.split(' '), (key) => { - object.on(key.slice(2).toLowerCase(), value); - }); - } else if (!_.has(object, key)) { - object[key] = cloneDeep(value); - } - } - }); - } - - /* ------------------------------------------------------------------------*/ - - /** - * Handles cycling/completing the deferred benchmark. - * - * @memberOf Benchmark.Deferred + * @memberOf timer + * @param {Object} deferred The deferred instance. */ - function resolve() { - let deferred = this, - clone = deferred.benchmark, - bench = clone._original; - - if (bench.aborted) { - // cycle() -> clone cycle/complete event -> compute()'s invoked bench.run() cycle/complete. - deferred.teardown(); - clone.running = false; - cycle(deferred); - } else if (++deferred.cycles < clone.count) { - clone.compiled.call(deferred, context, timer); - } else { - timer.stop(deferred); - deferred.teardown(); - delay(clone, () => { - cycle(deferred); - }); - } - } + stop: null, // Lazy defined in `clock()`. + } - /* ------------------------------------------------------------------------*/ + /* ------------------------------------------------------------------------*/ - /** - * A generic `Array#filter` like method. - * - * @static - * @memberOf Benchmark - * @param {Array} array The array to iterate over. - * @param {Function|string} callback The function/alias called per iteration. - * @returns {Array} A new array of values that passed callback filter. - * @example - * - * // get odd numbers - * Benchmark.filter([1, 2, 3, 4, 5], function(n) { - * return n % 2; - * }); // -> [1, 3, 5]; - * - * // get fastest benchmarks - * Benchmark.filter(benches, 'fastest'); - * - * // get slowest benchmarks - * Benchmark.filter(benches, 'slowest'); - * - * // get benchmarks that completed without erroring - * Benchmark.filter(benches, 'successful'); - */ - function filter(array, callback) { - if (callback === 'successful') { - // Callback to exclude those that are errored, unrun, or have hz of Infinity. - callback = function(bench) { - return bench.cycles && _.isFinite(bench.hz) && !bench.error; - }; - } else if (callback === 'fastest' || callback === 'slowest') { - // Get successful, sort by period + margin of error, and filter fastest/slowest. - const result = filter(array, 'successful').sort((a, b) => { - a = a.stats; b = b.stats; - - return (a.mean + a.moe > b.mean + b.moe ? 1 : -1) * (callback === 'fastest' ? 1 : -1); - }); - - return _.filter(result, bench => result[0].compare(bench) == 0); - } - - return _.filter(array, callback); - } + /** + * The Benchmark constructor. + * + * Note: The Benchmark constructor exposes a handful of lodash methods to + * make working with arrays, collections, and objects easier. The lodash + * methods are: + * [`each/forEach`](https://lodash.com/docs#forEach), [`forOwn`](https://lodash.com/docs#forOwn), + * [`has`](https://lodash.com/docs#has), [`indexOf`](https://lodash.com/docs#indexOf), + * [`map`](https://lodash.com/docs#map), and [`reduce`](https://lodash.com/docs#reduce) + * + * @constructor + * @param {string} name A name to identify the benchmark. + * @param {Function|string} fn The test to benchmark. + * @param {Object} [options={}] Options object. + * @example + * + * // basic usage (the `new` operator is optional) + * var bench = new Benchmark(fn); + * + * // or using a name first + * var bench = new Benchmark('foo', fn); + * + * // or with options + * var bench = new Benchmark('foo', fn, { + * + * // displayed by `Benchmark#toString` if `name` is not available + * 'id': 'xyz', + * + * // called when the benchmark starts running + * 'onStart': onStart, + * + * // called after each run cycle + * 'onCycle': onCycle, + * + * // called when aborted + * 'onAbort': onAbort, + * + * // called when a test errors + * 'onError': onError, + * + * // called when reset + * 'onReset': onReset, + * + * // called when the benchmark completes running + * 'onComplete': onComplete, + * + * // compiled/called before the test loop + * 'setup': setup, + * + * // compiled/called after the test loop + * 'teardown': teardown + * }); + * + * // or name and options + * var bench = new Benchmark('foo', { + * + * // a flag to indicate the benchmark is deferred + * 'defer': true, + * + * // benchmark test function + * 'fn': function(deferred) { + * // call `Deferred#resolve` when the deferred test is finished + * deferred.resolve(); + * } + * }); + * + * // or options only + * var bench = new Benchmark({ + * + * // benchmark name + * 'name': 'foo', + * + * // benchmark test as a string + * 'fn': '[1,2,3,4].sort()' + * }); + * + * // a test's `this` binding is set to the benchmark instance + * var bench = new Benchmark('foo', function() { + * 'My name is '.concat(this.name); // "My name is foo" + * }); + */ + function Benchmark(name, fn, options) { + const bench = this + + // Allow instance creation without the `new` operator. + if (!(bench instanceof Benchmark)) { + return new Benchmark(name, fn, options) + } + // Juggle arguments. + if (_.isPlainObject(name)) { + // 1 argument (options). + options = name + } else if (_.isFunction(name)) { + // 2 arguments (fn, options). + options = fn + fn = name + } else if (_.isPlainObject(fn)) { + // 2 arguments (name, options). + options = fn + fn = null + bench.name = name + } else { + // 3 arguments (name, fn [, options]). + bench.name = name + } + setOptions(bench, options) + + bench.id || (bench.id = ++counter) + bench.fn == null && (bench.fn = fn) + + bench.stats = cloneDeep(bench.stats) + bench.times = cloneDeep(bench.times) + } - /** - * Converts a number to a more readable comma-separated string representation. - * - * @static - * @memberOf Benchmark - * @param {number} number The number to convert. - * @returns {string} The more readable string representation. - */ - function formatNumber(number) { - number = String(number).split('.'); + /** + * The Deferred constructor. + * + * @constructor + * @memberOf Benchmark + * @param {Object} clone The cloned benchmark instance. + */ + function Deferred(clone) { + const deferred = this + + if (!(deferred instanceof Deferred)) { + return new Deferred(clone) + } + deferred.benchmark = clone + clock(deferred) + } - return number[0].replace(/(?=(?:\d{3})+$)(?!\b)/g, ',') + - (number[1] ? '.' + number[1] : ''); - } + /** + * The Event constructor. + * + * @constructor + * @memberOf Benchmark + * @param {Object|string} type The event type. + */ + function Event(type) { + const event = this + + if (type instanceof Event) { + return type + } + + return event instanceof Event + ? _.assign( + event, + { timeStamp: _.now() }, + typeof type == 'string' ? { type: type } : type + ) + : new Event(type) + } - /** - * Invokes a method on all items in an array. - * - * @static - * @memberOf Benchmark - * @param {Array} benches Array of benchmarks to iterate over. - * @param {Object|string} name The name of the method to invoke OR options object. - * @param {...*} [args] Arguments to invoke the method with. - * @returns {Array} A new array of values returned from each method invoked. - * @example - * - * // invoke `reset` on all benchmarks - * Benchmark.invoke(benches, 'reset'); - * - * // invoke `emit` with arguments - * Benchmark.invoke(benches, 'emit', 'complete', listener); - * - * // invoke `run(true)`, treat benchmarks as a queue, and register invoke callbacks - * Benchmark.invoke(benches, { - * - * // invoke the `run` method - * 'name': 'run', - * - * // pass a single argument - * 'args': true, - * - * // treat as queue, removing benchmarks from front of `benches` until empty - * 'queued': true, - * - * // called before any benchmarks have been invoked. - * 'onStart': onStart, - * - * // called between invoking benchmarks - * 'onCycle': onCycle, - * - * // called after all benchmarks have been invoked. - * 'onComplete': onComplete - * }); - */ - function invoke(benches, name) { - let args, - bench, - queued, - index = -1, - eventProps = { 'currentTarget': benches }, - options = { - 'onStart': _.noop, - 'onCycle': _.noop, - 'onComplete': _.noop - }, - result = _.toArray(benches); - - /** - * Invokes the method of the current object and if synchronous, fetches the next. - */ - function execute() { - let listeners, - async = isAsync(bench); - - if (async) { - // Use `getNext` as the first listener. - bench.on('complete', getNext); - listeners = bench.events.complete; - listeners.splice(0, 0, listeners.pop()); - } - // Execute method. - result[index] = _.isFunction(bench && bench[name]) ? bench[name].apply(bench, args) : undefined; + /** + * The Suite constructor. + * + * Note: Each Suite instance has a handful of wrapped lodash methods to + * make working with Suites easier. The wrapped lodash methods are: + * [`each/forEach`](https://lodash.com/docs#forEach), [`indexOf`](https://lodash.com/docs#indexOf), + * [`map`](https://lodash.com/docs#map), and [`reduce`](https://lodash.com/docs#reduce) + * + * @constructor + * @memberOf Benchmark + * @param {string} name A name to identify the suite. + * @param {Object} [options={}] Options object. + * @example + * + * // basic usage (the `new` operator is optional) + * var suite = new Benchmark.Suite; + * + * // or using a name first + * var suite = new Benchmark.Suite('foo'); + * + * // or with options + * var suite = new Benchmark.Suite('foo', { + * + * // called when the suite starts running + * 'onStart': onStart, + * + * // called between running benchmarks + * 'onCycle': onCycle, + * + * // called when aborted + * 'onAbort': onAbort, + * + * // called when a test errors + * 'onError': onError, + * + * // called when reset + * 'onReset': onReset, + * + * // called when the suite completes running + * 'onComplete': onComplete + * }); + */ + function Suite(name, options) { + const suite = this + + // Allow instance creation without the `new` operator. + if (!(suite instanceof Suite)) { + return new Suite(name, options) + } + // Juggle arguments. + if (_.isPlainObject(name)) { + // 1 argument (options). + options = name + } else { + // 2 arguments (name [, options]). + suite.name = name + } + setOptions(suite, options) + } - // If synchronous return `true` until finished. - return !async && getNext(); - } + /* ------------------------------------------------------------------------*/ - /** - * Fetches the next bench or executes `onComplete` callback. - */ - function getNext(event) { - let cycleEvent, - last = bench, - async = isAsync(last); - - if (async) { - last.off('complete', getNext); - last.emit('complete'); - } - // Emit "cycle" event. - eventProps.type = 'cycle'; - eventProps.target = last; - cycleEvent = Event(eventProps); - options.onCycle.call(benches, cycleEvent); - - // Choose next benchmark if not exiting early. - if (!cycleEvent.aborted && raiseIndex() !== false) { - bench = queued ? benches[0] : result[index]; - if (isAsync(bench)) { - delay(bench, execute); - } else if (async) { - // Resume execution if previously asynchronous but now synchronous. - while (execute()) {} - } else { - // Continue synchronous execution. - return true; - } - } else { - // Emit "complete" event. - eventProps.type = 'complete'; - options.onComplete.call(benches, Event(eventProps)); - } - // When used as a listener `event.aborted = true` will cancel the rest of - // the "complete" listeners because they were already called above and when - // used as part of `getNext` the `return false` will exit the execution while-loop. - if (event) { - event.aborted = true; - } else { - return false; - } - } + /** + * A specialized version of `_.cloneDeep` which only clones arrays and plain + * objects assigning all other values by reference. + * + * @private + * @param {*} value The value to clone. + * @returns {*} The cloned value. + */ + var cloneDeep = _.partial(_.cloneDeepWith, _, (value) => { + // Only clone primitives, arrays, and plain objects. + if (!_.isArray(value) && !_.isPlainObject(value)) { + return value + } + }) - /** - * Checks if invoking `Benchmark#run` with asynchronous cycles. - */ - function isAsync(object) { - // Avoid using `instanceof` here because of IE memory leak issues with host objects. - const async = args[0] && args[0].async; + /** + * Creates a function from the given arguments string and body. + * + * @private + * @param {string} args The comma separated function arguments. + * @param {string} body The function body. + * @returns {Function} The new function. + */ + function createFunction() { + // Lazy define. + createFunction = function (args, body) { + let result, + anchor = freeDefine ? freeDefine.amd : Benchmark, + prop = uid + 'createFunction' + + runScript( + (freeDefine ? 'define.amd.' : 'Benchmark.') + + prop + + '=function(' + + args + + '){' + + body + + '}' + ) + result = anchor[prop] + delete anchor[prop] + + return result + } + // Fix JaegerMonkey bug. + // For more information see http://bugzil.la/639720. + createFunction = + support.browser && + (createFunction('', 'return"' + uid + '"') || _.noop)() == uid + ? createFunction + : Function + + return createFunction.apply(null, arguments) + } - return name == 'run' && (object instanceof Benchmark) && - ((async == null ? object.options.async : async) && support.timeout || object.defer); - } + /** + * Delay the execution of a function based on the benchmark's `delay` property. + * + * @private + * @param {Object} bench The benchmark instance. + * @param {Object} fn The function to execute. + */ + function delay(bench, fn) { + bench._timerId = _.delay(fn, bench.delay * 1e3) + } - /** - * Raises `index` to the next defined index or returns `false`. - */ - function raiseIndex() { - index++; + /** + * Destroys the given element. + * + * @private + * @param {Element} element The element to destroy. + */ + function destroyElement(element) { + trash.appendChild(element) + trash.innerHTML = '' + } - // If queued remove the previous bench. - if (queued && index > 0) { - shift.call(benches); - } + /** + * Gets the name of the first argument from a function's source. + * + * @private + * @param {Function} fn The function. + * @returns {string} The argument name. + */ + function getFirstArgument(fn) { + return ( + (!_.has(fn, 'toString') && + (/^[\s(]*function[^(]*\(([^\s,)]+)/.exec(fn) || 0)[1]) || + '' + ) + } - // If we reached the last index then return `false`. - return (queued ? benches.length : index < result.length) ? - index : - (index = false); - } - // Juggle arguments. - if (_.isString(name)) { - // 2 arguments (array, name). - args = slice.call(arguments, 2); - } else { - // 2 arguments (array, options). - options = _.assign(options, name); - name = options.name; - args = _.isArray(args = 'args' in options ? options.args : []) ? args : [args]; - queued = options.queued; - } - // Start iterating over the array. - if (raiseIndex() !== false) { - // Emit "start" event. - bench = result[index]; - eventProps.type = 'start'; - eventProps.target = bench; - options.onStart.call(benches, Event(eventProps)); - - // End early if the suite was aborted in an "onStart" listener. - if (name == 'run' && (benches instanceof Suite) && benches.aborted) { - // Emit "cycle" event. - eventProps.type = 'cycle'; - options.onCycle.call(benches, Event(eventProps)); - // Emit "complete" event. - eventProps.type = 'complete'; - options.onComplete.call(benches, Event(eventProps)); - } - // Start method execution. - else if (isAsync(bench)) { - delay(bench, execute); - } else { - while (execute()) {} - } - } + /** + * Computes the arithmetic mean of a sample. + * + * @private + * @param {Array} sample The sample. + * @returns {number} The mean. + */ + function getMean(sample) { + return _.reduce(sample, (sum, x) => sum + x) / sample.length || 0 + } - return result; - } + /** + * Gets the source code of a function. + * + * @private + * @param {Function} fn The function. + * @returns {string} The function's source code. + */ + function getSource(fn) { + let result = '' + + if (isStringable(fn)) { + result = String(fn) + } else if (support.decompilation) { + // Escape the `{` for Firefox 1. + result = _.result(/^[^{]+\{([\s\S]*)\}\s*$/.exec(fn), 1) + } + // Trim string. + result = (result || '').replace(/^\s+|\s+$/g, '') + + // Detect strings containing only the "use strict" directive. + return /^(?:\/\*+[\w\W]*?\*\/|\/\/.*?[\n\r\u2028\u2029]|\s)*(["'])use strict\1;?$/.test( + result + ) + ? '' + : result + } - /** - * Creates a string of joined array values or object key-value pairs. - * - * @static - * @memberOf Benchmark - * @param {Array|Object} object The object to operate on. - * @param {string} [separator1=','] The separator used between key-value pairs. - * @param {string} [separator2=': '] The separator used between keys and values. - * @returns {string} The joined result. - */ - function join(object, separator1, separator2) { - let result = [], - length = (object = Object(object)).length, - arrayLike = length === length >>> 0; + /** + * Checks if an object is of the specified class. + * + * @private + * @param {*} value The value to check. + * @param {string} name The name of the class. + * @returns {boolean} Returns `true` if the value is of the specified class, else `false`. + */ + function isClassOf(value, name) { + return value != null && toString.call(value) == '[object ' + name + ']' + } - separator2 || (separator2 = ': '); - _.each(object, (value, key) => { - result.push(arrayLike ? value : key + separator2 + value); - }); + /** + * Host objects can return type values that are different from their actual + * data type. The objects we are concerned with usually return non-primitive + * types of "object", "function", or "unknown". + * + * @private + * @param {*} object The owner of the property. + * @param {string} property The property to check. + * @returns {boolean} Returns `true` if the property value is a non-primitive, else `false`. + */ + function isHostType(object, property) { + if (object == null) { + return false + } + const type = typeof object[property] + + return ( + !rePrimitive.test(type) && + (type != 'object' || Boolean(object[property])) + ) + } - return result.join(separator1 || ','); - } + /** + * Checks if a value can be safely coerced to a string. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the value can be coerced, else `false`. + */ + function isStringable(value) { + return ( + _.isString(value) || + (_.has(value, 'toString') && _.isFunction(value.toString)) + ) + } - /* ------------------------------------------------------------------------*/ + /** + * A wrapper around `require` to suppress `module missing` errors. + * + * @private + * @param {string} id The module id. + * @returns {*} The exported module or `null`. + */ + function require(id) { + try { + var result = freeExports && freeRequire(id) + } catch (e) {} - /** - * Aborts all benchmarks in the suite. - * - * @name abort - * @memberOf Benchmark.Suite - * @returns {Object} The suite instance. - */ - function abortSuite() { - let event, - suite = this, - resetting = calledBy.resetSuite; - - if (suite.running) { - event = Event('abort'); - suite.emit(event); - if (!event.cancelled || resetting) { - // Avoid infinite recursion. - calledBy.abortSuite = true; - suite.reset(); - delete calledBy.abortSuite; - - if (!resetting) { - suite.aborted = true; - invoke(suite, 'abort'); - } - } - } + return result || null + } - return suite; + /** + * Runs a snippet of JavaScript via script injection. + * + * @private + * @param {string} code The code to run. + */ + function runScript(code) { + let anchor = freeDefine ? define.amd : Benchmark, + script = doc.createElement('script'), + sibling = doc.getElementsByTagName('script')[0], + parent = sibling.parentNode, + prop = uid + 'runScript', + prefix = + '(' + + (freeDefine ? 'define.amd.' : 'Benchmark.') + + prop + + '||function(){})();' + + // Firefox 2.0.0.2 cannot use script injection as intended because it executes + // asynchronously, but that's OK because script injection is only used to avoid + // the previously commented JaegerMonkey bug. + try { + // Remove the inserted script *before* running the code to avoid differences + // in the expected script element count/order of the document. + script.appendChild(doc.createTextNode(prefix + code)) + anchor[prop] = function () { + destroyElement(script) } + } catch (e) { + parent = parent.cloneNode(false) + sibling = null + script.text = code + } + parent.insertBefore(script, sibling) + delete anchor[prop] + } - /** - * Adds a test to the benchmark suite. - * - * @memberOf Benchmark.Suite - * @param {string} name A name to identify the benchmark. - * @param {Function|string} fn The test to benchmark. - * @param {Object} [options={}] Options object. - * @returns {Object} The suite instance. - * @example - * - * // basic usage - * suite.add(fn); - * - * // or using a name first - * suite.add('foo', fn); - * - * // or with options - * suite.add('foo', fn, { - * 'onCycle': onCycle, - * 'onComplete': onComplete - * }); - * - * // or name and options - * suite.add('foo', { - * 'fn': fn, - * 'onCycle': onCycle, - * 'onComplete': onComplete - * }); - * - * // or options only - * suite.add({ - * 'name': 'foo', - * 'fn': fn, - * 'onCycle': onCycle, - * 'onComplete': onComplete - * }); - */ - function add(name, fn, options) { - let suite = this, - bench = new Benchmark(name, fn, options), - event = Event({ - 'type': 'add', - 'target': bench - }); - - if (suite.emit(event), !event.cancelled) { - suite.push(bench); - } - - return suite; + /** + * A helper function for setting options/event handlers. + * + * @private + * @param {Object} object The benchmark or suite instance. + * @param {Object} [options={}] Options object. + */ + function setOptions(object, options) { + options = object.options = _.assign( + {}, + cloneDeep(object.constructor.options), + cloneDeep(options) + ) + + _.forOwn(options, (value, key) => { + if (value != null) { + // Add event listeners. + if (/^on[A-Z]/.test(key)) { + _.each(key.split(' '), (key) => { + object.on(key.slice(2).toLowerCase(), value) + }) + } else if (!_.has(object, key)) { + object[key] = cloneDeep(value) + } } + }) + } - /** - * Creates a new suite with cloned benchmarks. - * - * @name clone - * @memberOf Benchmark.Suite - * @param {Object} options Options object to overwrite cloned options. - * @returns {Object} The new suite instance. - */ - function cloneSuite(options) { - let suite = this, - result = new suite.constructor(_.assign({}, suite.options, options)); - - // Copy own properties. - _.forOwn(suite, (value, key) => { - if (!_.has(result, key)) { - result[key] = _.isFunction(_.get(value, 'clone')) ? - value.clone() : - cloneDeep(value); - } - }); - - return result; - } + /* ------------------------------------------------------------------------*/ - /** - * An `Array#filter` like method. - * - * @name filter - * @memberOf Benchmark.Suite - * @param {Function|string} callback The function/alias called per iteration. - * @returns {Object} A new suite of benchmarks that passed callback filter. - */ - function filterSuite(callback) { - let suite = this, - result = new suite.constructor(suite.options); + /** + * Handles cycling/completing the deferred benchmark. + * + * @memberOf Benchmark.Deferred + */ + function resolve() { + let deferred = this, + clone = deferred.benchmark, + bench = clone._original + + if (bench.aborted) { + // cycle() -> clone cycle/complete event -> compute()'s invoked bench.run() cycle/complete. + deferred.teardown() + clone.running = false + cycle(deferred) + } else if (++deferred.cycles < clone.count) { + clone.compiled.call(deferred, context, timer) + } else { + timer.stop(deferred) + deferred.teardown() + delay(clone, () => { + cycle(deferred) + }) + } + } - result.push.apply(result, filter(suite, callback)); + /* ------------------------------------------------------------------------*/ - return result; + /** + * A generic `Array#filter` like method. + * + * @static + * @memberOf Benchmark + * @param {Array} array The array to iterate over. + * @param {Function|string} callback The function/alias called per iteration. + * @returns {Array} A new array of values that passed callback filter. + * @example + * + * // get odd numbers + * Benchmark.filter([1, 2, 3, 4, 5], function(n) { + * return n % 2; + * }); // -> [1, 3, 5]; + * + * // get fastest benchmarks + * Benchmark.filter(benches, 'fastest'); + * + * // get slowest benchmarks + * Benchmark.filter(benches, 'slowest'); + * + * // get benchmarks that completed without erroring + * Benchmark.filter(benches, 'successful'); + */ + function filter(array, callback) { + if (callback === 'successful') { + // Callback to exclude those that are errored, unrun, or have hz of Infinity. + callback = function (bench) { + return bench.cycles && _.isFinite(bench.hz) && !bench.error } + } else if (callback === 'fastest' || callback === 'slowest') { + // Get successful, sort by period + margin of error, and filter fastest/slowest. + const result = filter(array, 'successful').sort((a, b) => { + a = a.stats + b = b.stats + + return ( + (a.mean + a.moe > b.mean + b.moe ? 1 : -1) * + (callback === 'fastest' ? 1 : -1) + ) + }) + + return _.filter(result, (bench) => result[0].compare(bench) == 0) + } + + return _.filter(array, callback) + } - /** - * Resets all benchmarks in the suite. - * - * @name reset - * @memberOf Benchmark.Suite - * @returns {Object} The suite instance. - */ - function resetSuite() { - let event, - suite = this, - aborting = calledBy.abortSuite; - - if (suite.running && !aborting) { - // No worries, `resetSuite()` is called within `abortSuite()`. - calledBy.resetSuite = true; - suite.abort(); - delete calledBy.resetSuite; - } - // Reset if the state has changed. - else if ((suite.aborted || suite.running) && - (suite.emit(event = Event('reset')), !event.cancelled)) { - suite.aborted = suite.running = false; - if (!aborting) { - invoke(suite, 'reset'); - } - } + /** + * Converts a number to a more readable comma-separated string representation. + * + * @static + * @memberOf Benchmark + * @param {number} number The number to convert. + * @returns {string} The more readable string representation. + */ + function formatNumber(number) { + number = String(number).split('.') - return suite; - } + return ( + number[0].replace(/(?=(?:\d{3})+$)(?!\b)/g, ',') + + (number[1] ? '.' + number[1] : '') + ) + } - /** - * Runs the suite. - * - * @name run - * @memberOf Benchmark.Suite - * @param {Object} [options={}] Options object. - * @returns {Object} The suite instance. - * @example - * - * // basic usage - * suite.run(); - * - * // or with options - * suite.run({ 'async': true, 'queued': true }); + /** + * Invokes a method on all items in an array. + * + * @static + * @memberOf Benchmark + * @param {Array} benches Array of benchmarks to iterate over. + * @param {Object|string} name The name of the method to invoke OR options object. + * @param {...*} [args] Arguments to invoke the method with. + * @returns {Array} A new array of values returned from each method invoked. + * @example + * + * // invoke `reset` on all benchmarks + * Benchmark.invoke(benches, 'reset'); + * + * // invoke `emit` with arguments + * Benchmark.invoke(benches, 'emit', 'complete', listener); + * + * // invoke `run(true)`, treat benchmarks as a queue, and register invoke callbacks + * Benchmark.invoke(benches, { + * + * // invoke the `run` method + * 'name': 'run', + * + * // pass a single argument + * 'args': true, + * + * // treat as queue, removing benchmarks from front of `benches` until empty + * 'queued': true, + * + * // called before any benchmarks have been invoked. + * 'onStart': onStart, + * + * // called between invoking benchmarks + * 'onCycle': onCycle, + * + * // called after all benchmarks have been invoked. + * 'onComplete': onComplete + * }); + */ + function invoke(benches, name) { + let args, + bench, + queued, + index = -1, + eventProps = { currentTarget: benches }, + options = { + onStart: _.noop, + onCycle: _.noop, + onComplete: _.noop, + }, + result = _.toArray(benches) + + /** + * Invokes the method of the current object and if synchronous, fetches the next. */ - function runSuite(options) { - const suite = this; - - suite.reset(); - suite.running = true; - options || (options = {}); - - invoke(suite, { - 'name': 'run', - 'args': options, - 'queued': options.queued, - 'onStart': function(event) { - suite.emit(event); - }, - 'onCycle': function(event) { - const bench = event.target; - - if (bench.error) { - suite.emit({ - 'type': 'error', - 'target': bench - }); - } - suite.emit(event); - event.aborted = suite.aborted; - }, - 'onComplete': function(event) { - suite.running = false; - suite.emit(event); - } - }); - - return suite; + function execute() { + let listeners, + async = isAsync(bench) + + if (async) { + // Use `getNext` as the first listener. + bench.on('complete', getNext) + listeners = bench.events.complete + listeners.splice(0, 0, listeners.pop()) } + // Execute method. + result[index] = _.isFunction(bench && bench[name]) + ? bench[name].apply(bench, args) + : undefined - /* ------------------------------------------------------------------------*/ + // If synchronous return `true` until finished. + return !async && getNext() + } - /** - * Executes all registered listeners of the specified event type. - * - * @memberOf Benchmark, Benchmark.Suite - * @param {Object|string} type The event type or object. - * @param {...*} [args] Arguments to invoke the listener with. - * @returns {*} Returns the return value of the last listener executed. + /** + * Fetches the next bench or executes `onComplete` callback. */ - function emit(type) { - let listeners, - object = this, - event = Event(type), - events = object.events, - args = (arguments[0] = event, arguments); - - event.currentTarget || (event.currentTarget = object); - event.target || (event.target = object); - delete event.result; - - if (events && (listeners = _.has(events, event.type) && events[event.type])) { - _.each(listeners.slice(), (listener) => { - if ((event.result = listener.apply(object, args)) === false) { - event.cancelled = true; - } - - return !event.aborted; - }); - } - - return event.result; + function getNext(event) { + let cycleEvent, + last = bench, + async = isAsync(last) + + if (async) { + last.off('complete', getNext) + last.emit('complete') + } + // Emit "cycle" event. + eventProps.type = 'cycle' + eventProps.target = last + cycleEvent = Event(eventProps) + options.onCycle.call(benches, cycleEvent) + + // Choose next benchmark if not exiting early. + if (!cycleEvent.aborted && raiseIndex() !== false) { + bench = queued ? benches[0] : result[index] + if (isAsync(bench)) { + delay(bench, execute) + } else if (async) { + // Resume execution if previously asynchronous but now synchronous. + while (execute()) {} + } else { + // Continue synchronous execution. + return true + } + } else { + // Emit "complete" event. + eventProps.type = 'complete' + options.onComplete.call(benches, Event(eventProps)) + } + // When used as a listener `event.aborted = true` will cancel the rest of + // the "complete" listeners because they were already called above and when + // used as part of `getNext` the `return false` will exit the execution while-loop. + if (event) { + event.aborted = true + } else { + return false } + } - /** - * Returns an array of event listeners for a given type that can be manipulated - * to add or remove listeners. - * - * @memberOf Benchmark, Benchmark.Suite - * @param {string} type The event type. - * @returns {Array} The listeners array. + /** + * Checks if invoking `Benchmark#run` with asynchronous cycles. + */ + function isAsync(object) { + // Avoid using `instanceof` here because of IE memory leak issues with host objects. + const async = args[0] && args[0].async + + return ( + name == 'run' && + object instanceof Benchmark && + (((async == null ? object.options.async : async) && + support.timeout) || + object.defer) + ) + } + + /** + * Raises `index` to the next defined index or returns `false`. */ - function listeners(type) { - let object = this, - events = object.events || (object.events = {}); + function raiseIndex() { + index++ - return _.has(events, type) ? events[type] : (events[type] = []); + // If queued remove the previous bench. + if (queued && index > 0) { + shift.call(benches) } - /** - * Unregisters a listener for the specified event type(s), - * or unregisters all listeners for the specified event type(s), - * or unregisters all listeners for all event types. - * - * @memberOf Benchmark, Benchmark.Suite - * @param {string} [type] The event type. - * @param {Function} [listener] The function to unregister. - * @returns {Object} The current instance. - * @example - * - * // unregister a listener for an event type - * bench.off('cycle', listener); - * - * // unregister a listener for multiple event types - * bench.off('start cycle', listener); - * - * // unregister all listeners for an event type - * bench.off('cycle'); - * - * // unregister all listeners for multiple event types - * bench.off('start cycle complete'); - * - * // unregister all listeners for all event types - * bench.off(); - */ - function off(type, listener) { - let object = this, - events = object.events; + // If we reached the last index then return `false`. + return (queued ? benches.length : index < result.length) + ? index + : (index = false) + } + // Juggle arguments. + if (_.isString(name)) { + // 2 arguments (array, name). + args = slice.call(arguments, 2) + } else { + // 2 arguments (array, options). + options = _.assign(options, name) + name = options.name + args = _.isArray((args = 'args' in options ? options.args : [])) + ? args + : [args] + queued = options.queued + } + // Start iterating over the array. + if (raiseIndex() !== false) { + // Emit "start" event. + bench = result[index] + eventProps.type = 'start' + eventProps.target = bench + options.onStart.call(benches, Event(eventProps)) + + // End early if the suite was aborted in an "onStart" listener. + if (name == 'run' && benches instanceof Suite && benches.aborted) { + // Emit "cycle" event. + eventProps.type = 'cycle' + options.onCycle.call(benches, Event(eventProps)) + // Emit "complete" event. + eventProps.type = 'complete' + options.onComplete.call(benches, Event(eventProps)) + } + // Start method execution. + else if (isAsync(bench)) { + delay(bench, execute) + } else { + while (execute()) {} + } + } - if (!events) { - return object; - } - _.each(type ? type.split(' ') : events, (listeners, type) => { - let index; + return result + } - if (typeof listeners == 'string') { - type = listeners; - listeners = _.has(events, type) && events[type]; - } - if (listeners) { - if (listener) { - index = _.indexOf(listeners, listener); - if (index > -1) { - listeners.splice(index, 1); - } - } else { - listeners.length = 0; - } - } - }); + /** + * Creates a string of joined array values or object key-value pairs. + * + * @static + * @memberOf Benchmark + * @param {Array|Object} object The object to operate on. + * @param {string} [separator1=','] The separator used between key-value pairs. + * @param {string} [separator2=': '] The separator used between keys and values. + * @returns {string} The joined result. + */ + function join(object, separator1, separator2) { + let result = [], + length = (object = Object(object)).length, + arrayLike = length === length >>> 0 - return object; - } + separator2 || (separator2 = ': ') + _.each(object, (value, key) => { + result.push(arrayLike ? value : key + separator2 + value) + }) - /** - * Registers a listener for the specified event type(s). - * - * @memberOf Benchmark, Benchmark.Suite - * @param {string} type The event type. - * @param {Function} listener The function to register. - * @returns {Object} The current instance. - * @example - * - * // register a listener for an event type - * bench.on('cycle', listener); - * - * // register a listener for multiple event types - * bench.on('start cycle', listener); - */ - function on(type, listener) { - let object = this, - events = object.events || (object.events = {}); - - _.each(type.split(' '), (type) => { - (_.has(events, type) ? - events[type] : - (events[type] = []) - ).push(listener); - }); - - return object; + return result.join(separator1 || ',') + } + + /* ------------------------------------------------------------------------*/ + + /** + * Aborts all benchmarks in the suite. + * + * @name abort + * @memberOf Benchmark.Suite + * @returns {Object} The suite instance. + */ + function abortSuite() { + let event, + suite = this, + resetting = calledBy.resetSuite + + if (suite.running) { + event = Event('abort') + suite.emit(event) + if (!event.cancelled || resetting) { + // Avoid infinite recursion. + calledBy.abortSuite = true + suite.reset() + delete calledBy.abortSuite + + if (!resetting) { + suite.aborted = true + invoke(suite, 'abort') + } } + } - /* ------------------------------------------------------------------------*/ + return suite + } - /** - * Aborts the benchmark without recording times. - * - * @memberOf Benchmark - * @returns {Object} The benchmark instance. - */ - function abort() { - let event, - bench = this, - resetting = calledBy.reset; - - if (bench.running) { - event = Event('abort'); - bench.emit(event); - if (!event.cancelled || resetting) { - // Avoid infinite recursion. - calledBy.abort = true; - bench.reset(); - delete calledBy.abort; - - if (support.timeout) { - clearTimeout(bench._timerId); - delete bench._timerId; - } - if (!resetting) { - bench.aborted = true; - bench.running = false; - } - } - } + /** + * Adds a test to the benchmark suite. + * + * @memberOf Benchmark.Suite + * @param {string} name A name to identify the benchmark. + * @param {Function|string} fn The test to benchmark. + * @param {Object} [options={}] Options object. + * @returns {Object} The suite instance. + * @example + * + * // basic usage + * suite.add(fn); + * + * // or using a name first + * suite.add('foo', fn); + * + * // or with options + * suite.add('foo', fn, { + * 'onCycle': onCycle, + * 'onComplete': onComplete + * }); + * + * // or name and options + * suite.add('foo', { + * 'fn': fn, + * 'onCycle': onCycle, + * 'onComplete': onComplete + * }); + * + * // or options only + * suite.add({ + * 'name': 'foo', + * 'fn': fn, + * 'onCycle': onCycle, + * 'onComplete': onComplete + * }); + */ + function add(name, fn, options) { + let suite = this, + bench = new Benchmark(name, fn, options), + event = Event({ + type: 'add', + target: bench, + }) + + if ((suite.emit(event), !event.cancelled)) { + suite.push(bench) + } + + return suite + } - return bench; + /** + * Creates a new suite with cloned benchmarks. + * + * @name clone + * @memberOf Benchmark.Suite + * @param {Object} options Options object to overwrite cloned options. + * @returns {Object} The new suite instance. + */ + function cloneSuite(options) { + let suite = this, + result = new suite.constructor(_.assign({}, suite.options, options)) + + // Copy own properties. + _.forOwn(suite, (value, key) => { + if (!_.has(result, key)) { + result[key] = _.isFunction(_.get(value, 'clone')) + ? value.clone() + : cloneDeep(value) } + }) - /** - * Creates a new benchmark using the same test and options. - * - * @memberOf Benchmark - * @param {Object} options Options object to overwrite cloned options. - * @returns {Object} The new benchmark instance. - * @example - * - * var bizarro = bench.clone({ - * 'name': 'doppelganger' - * }); - */ - function clone(options) { - let bench = this, - result = new bench.constructor(_.assign({}, bench, options)); + return result + } - // Correct the `options` object. - result.options = _.assign({}, cloneDeep(bench.options), cloneDeep(options)); + /** + * An `Array#filter` like method. + * + * @name filter + * @memberOf Benchmark.Suite + * @param {Function|string} callback The function/alias called per iteration. + * @returns {Object} A new suite of benchmarks that passed callback filter. + */ + function filterSuite(callback) { + let suite = this, + result = new suite.constructor(suite.options) - // Copy own custom properties. - _.forOwn(bench, (value, key) => { - if (!_.has(result, key)) { - result[key] = cloneDeep(value); - } - }); + result.push.apply(result, filter(suite, callback)) - return result; - } + return result + } - /** - * Determines if a benchmark is faster than another. - * - * @memberOf Benchmark - * @param {Object} other The benchmark to compare. - * @returns {number} Returns `-1` if slower, `1` if faster, and `0` if indeterminate. - */ - function compare(other) { - const bench = this; + /** + * Resets all benchmarks in the suite. + * + * @name reset + * @memberOf Benchmark.Suite + * @returns {Object} The suite instance. + */ + function resetSuite() { + let event, + suite = this, + aborting = calledBy.abortSuite + + if (suite.running && !aborting) { + // No worries, `resetSuite()` is called within `abortSuite()`. + calledBy.resetSuite = true + suite.abort() + delete calledBy.resetSuite + } + // Reset if the state has changed. + else if ( + (suite.aborted || suite.running) && + (suite.emit((event = Event('reset'))), !event.cancelled) + ) { + suite.aborted = suite.running = false + if (!aborting) { + invoke(suite, 'reset') + } + } - // Exit early if comparing the same benchmark. - if (bench == other) { - return 0; - } - let critical, - zStat, - sample1 = bench.stats.sample, - sample2 = other.stats.sample, - size1 = sample1.length, - size2 = sample2.length, - maxSize = max(size1, size2), - minSize = min(size1, size2), - u1 = getU(sample1, sample2), - u2 = getU(sample2, sample1), - u = min(u1, u2); - - function getScore(xA, sampleB) { - return _.reduce(sampleB, (total, xB) => total + (xB > xA ? 0 : xB < xA ? 1 : 0.5), 0); - } + return suite + } - function getU(sampleA, sampleB) { - return _.reduce(sampleA, (total, xA) => total + getScore(xA, sampleB), 0); - } + /** + * Runs the suite. + * + * @name run + * @memberOf Benchmark.Suite + * @param {Object} [options={}] Options object. + * @returns {Object} The suite instance. + * @example + * + * // basic usage + * suite.run(); + * + * // or with options + * suite.run({ 'async': true, 'queued': true }); + */ + function runSuite(options) { + const suite = this + + suite.reset() + suite.running = true + options || (options = {}) + + invoke(suite, { + name: 'run', + args: options, + queued: options.queued, + onStart: function (event) { + suite.emit(event) + }, + onCycle: function (event) { + const bench = event.target + + if (bench.error) { + suite.emit({ + type: 'error', + target: bench, + }) + } + suite.emit(event) + event.aborted = suite.aborted + }, + onComplete: function (event) { + suite.running = false + suite.emit(event) + }, + }) + + return suite + } - function getZ(u) { - return (u - ((size1 * size2) / 2)) / sqrt((size1 * size2 * (size1 + size2 + 1)) / 12); - } - // Reject the null hypothesis the two samples come from the - // same population (i.e. have the same median) if... - if (size1 + size2 > 30) { - // ...the z-stat is greater than 1.96 or less than -1.96 - // http://www.statisticslectures.com/topics/mannwhitneyu/ - zStat = getZ(u); - - return abs(zStat) > 1.96 ? (u == u1 ? 1 : -1) : 0; - } - // ...the U value is less than or equal the critical U value. - critical = maxSize < 5 || minSize < 3 ? 0 : uTable[maxSize][minSize - 3]; + /* ------------------------------------------------------------------------*/ - return u <= critical ? (u == u1 ? 1 : -1) : 0; - } + /** + * Executes all registered listeners of the specified event type. + * + * @memberOf Benchmark, Benchmark.Suite + * @param {Object|string} type The event type or object. + * @param {...*} [args] Arguments to invoke the listener with. + * @returns {*} Returns the return value of the last listener executed. + */ + function emit(type) { + let listeners, + object = this, + event = Event(type), + events = object.events, + args = ((arguments[0] = event), arguments) + + event.currentTarget || (event.currentTarget = object) + event.target || (event.target = object) + delete event.result + + if ( + events && + (listeners = _.has(events, event.type) && events[event.type]) + ) { + _.each(listeners.slice(), (listener) => { + if ((event.result = listener.apply(object, args)) === false) { + event.cancelled = true + } + + return !event.aborted + }) + } + + return event.result + } - /** - * Reset properties and abort if running. - * - * @memberOf Benchmark - * @returns {Object} The benchmark instance. - */ - function reset() { - const bench = this; + /** + * Returns an array of event listeners for a given type that can be manipulated + * to add or remove listeners. + * + * @memberOf Benchmark, Benchmark.Suite + * @param {string} type The event type. + * @returns {Array} The listeners array. + */ + function listeners(type) { + let object = this, + events = object.events || (object.events = {}) - if (bench.running && !calledBy.abort) { - // No worries, `reset()` is called within `abort()`. - calledBy.reset = true; - bench.abort(); - delete calledBy.reset; + return _.has(events, type) ? events[type] : (events[type] = []) + } - return bench; - } - let event, - index = 0, - changes = [], - queue = []; - - // A non-recursive solution to check if properties have changed. - // For more information see http://www.jslab.dk/articles/non.recursive.preorder.traversal.part4. - let data = { - 'destination': bench, - 'source': _.assign({}, cloneDeep(bench.constructor.prototype), cloneDeep(bench.options)) - }; - - do { - _.forOwn(data.source, (value, key) => { - let changed, - destination = data.destination, - currValue = destination[key]; - - // Skip pseudo private properties and event listeners. - if (/^_|^events$|^on[A-Z]/.test(key)) { - return; - } - if (_.isObjectLike(value)) { - if (_.isArray(value)) { - // Check if an array value has changed to a non-array value. - if (!_.isArray(currValue)) { - changed = true; - currValue = []; - } - // Check if an array has changed its length. - if (currValue.length != value.length) { - changed = true; - currValue = currValue.slice(0, value.length); - currValue.length = value.length; - } - } - // Check if an object has changed to a non-object value. - else if (!_.isObjectLike(currValue)) { - changed = true; - currValue = {}; - } - // Register a changed object. - if (changed) { - changes.push({ - 'destination': destination, - 'key': key, - 'value': currValue - }); - } - queue.push({ - 'destination': currValue, - 'source': value - }); - } - // Register a changed primitive. - else if (!_.eq(currValue, value) && value !== undefined) { - changes.push({ - 'destination': destination, - 'key': key, - 'value': value - }); - } - }); - } - while ((data = queue[index++])); - - // If changed emit the `reset` event and if it isn't cancelled reset the benchmark. - if (changes.length && - (bench.emit(event = Event('reset')), !event.cancelled)) { - _.each(changes, (data) => { - data.destination[data.key] = data.value; - }); + /** + * Unregisters a listener for the specified event type(s), + * or unregisters all listeners for the specified event type(s), + * or unregisters all listeners for all event types. + * + * @memberOf Benchmark, Benchmark.Suite + * @param {string} [type] The event type. + * @param {Function} [listener] The function to unregister. + * @returns {Object} The current instance. + * @example + * + * // unregister a listener for an event type + * bench.off('cycle', listener); + * + * // unregister a listener for multiple event types + * bench.off('start cycle', listener); + * + * // unregister all listeners for an event type + * bench.off('cycle'); + * + * // unregister all listeners for multiple event types + * bench.off('start cycle complete'); + * + * // unregister all listeners for all event types + * bench.off(); + */ + function off(type, listener) { + let object = this, + events = object.events + + if (!events) { + return object + } + _.each(type ? type.split(' ') : events, (listeners, type) => { + let index + + if (typeof listeners == 'string') { + type = listeners + listeners = _.has(events, type) && events[type] + } + if (listeners) { + if (listener) { + index = _.indexOf(listeners, listener) + if (index > -1) { + listeners.splice(index, 1) } - - return bench; + } else { + listeners.length = 0 + } } + }) - /** - * Displays relevant benchmark information when coerced to a string. - * - * @name toString - * @memberOf Benchmark - * @returns {string} A string representation of the benchmark instance. - */ - function toStringBench() { - let bench = this, - error = bench.error, - hz = bench.hz, - id = bench.id, - stats = bench.stats, - size = stats.sample.length, - pm = '\xb1', - result = bench.name || (_.isNaN(id) ? id : ''); - - if (error) { - let errorStr; - - if (!_.isObject(error)) { - errorStr = String(error); - } else if (!_.isError(Error)) { - errorStr = join(error); - } else { - // Error#name and Error#message properties are non-enumerable. - errorStr = join(_.assign({ - 'name': error.name, - 'message': error.message - }, error)); - } - result += ': ' + errorStr; - } else { - result += ' x ' + formatNumber(hz.toFixed(hz < 100 ? 2 : 0)) + ' ops/sec ' + pm + - stats.rme.toFixed(2) + '% (' + size + ' run' + (size == 1 ? '' : 's') + ' sampled)'; - } + return object + } - return result; - } + /** + * Registers a listener for the specified event type(s). + * + * @memberOf Benchmark, Benchmark.Suite + * @param {string} type The event type. + * @param {Function} listener The function to register. + * @returns {Object} The current instance. + * @example + * + * // register a listener for an event type + * bench.on('cycle', listener); + * + * // register a listener for multiple event types + * bench.on('start cycle', listener); + */ + function on(type, listener) { + let object = this, + events = object.events || (object.events = {}) - /* ------------------------------------------------------------------------*/ + _.each(type.split(' '), (type) => { + ;(_.has(events, type) ? events[type] : (events[type] = [])).push( + listener + ) + }) - /** - * Clocks the time taken to execute a test per cycle (secs). - * - * @private - * @param {Object} bench The benchmark instance. - * @returns {number} The time taken. - */ - function clock() { - let options = Benchmark.options, - templateData = {}, - timers = [{ - 'ns': timer.ns, - 'res': max(0.0015, getRes('ms')), - 'unit': 'ms' - }]; - - // Lazy define for hi-res timers. - clock = function(clone) { - let deferred; - - if (clone instanceof Deferred) { - deferred = clone; - clone = deferred.benchmark; - } - let bench = clone._original, - stringable = isStringable(bench.fn), - count = bench.count = clone.count, - decompilable = stringable || (support.decompilation && (clone.setup !== _.noop || clone.teardown !== _.noop)), - id = bench.id, - name = bench.name || (typeof id == 'number' ? '' : id), - result = 0; - - // Init `minTime` if needed. - clone.minTime = bench.minTime || (bench.minTime = bench.options.minTime = options.minTime); - - // Compile in setup/teardown functions and the test loop. - // Create a new compiled test, instead of using the cached `bench.compiled`, - // to avoid potential engine optimizations enabled over the life of the test. - let funcBody = deferred ? - 'var d#=this,${fnArg}=d#,m#=d#.benchmark._original,f#=m#.fn,su#=m#.setup,td#=m#.teardown;' + - // When `deferred.cycles` is `0` then... - 'if(!d#.cycles){' + - // set `deferred.fn`, - 'd#.fn=function(){var ${fnArg}=d#;if(typeof f#=="function"){try{${fn}\n}catch(e#){f#(d#)}}else{${fn}\n}};' + - // set `deferred.teardown`, - 'd#.teardown=function(){d#.cycles=0;if(typeof td#=="function"){try{${teardown}\n}catch(e#){td#()}}else{${teardown}\n}};' + - // execute the benchmark's `setup`, - 'if(typeof su#=="function"){try{${setup}\n}catch(e#){su#()}}else{${setup}\n};' + - // start timer, - 't#.start(d#);' + - // and then execute `deferred.fn` and return a dummy object. - '}d#.fn();return{uid:"${uid}"}' : - - 'var r#,s#,m#=this,f#=m#.fn,i#=m#.count,n#=t#.ns;${setup}\n${begin};' + - 'while(i#--){${fn}\n}${end};${teardown}\nreturn{elapsed:r#,uid:"${uid}"}'; - - let compiled = bench.compiled = clone.compiled = createCompiled(bench, decompilable, deferred, funcBody), - isEmpty = !(templateData.fn || stringable); - - try { - if (isEmpty) { - // Firefox may remove dead code from `Function#toString` results. - // For more information see http://bugzil.la/536085. - throw new Error('The test "' + name + '" is empty. This may be the result of dead code removal.'); - } else if (!deferred) { - // Pretest to determine if compiled code exits early, usually by a - // rogue `return` statement, by checking for a return object with the uid. - bench.count = 1; - compiled = decompilable && (compiled.call(bench, context, timer) || {}).uid == templateData.uid && compiled; - bench.count = count; - } - } catch (e) { - compiled = null; - clone.error = e || new Error(String(e)); - bench.count = count; - } - // Fallback when a test exits early or errors during pretest. - if (!compiled && !deferred && !isEmpty) { - funcBody = ( - stringable || (decompilable && !clone.error) ? - 'function f#(){${fn}\n}var r#,s#,m#=this,i#=m#.count' : - 'var r#,s#,m#=this,f#=m#.fn,i#=m#.count' - ) + - ',n#=t#.ns;${setup}\n${begin};m#.f#=f#;while(i#--){m#.f#()}${end};' + - 'delete m#.f#;${teardown}\nreturn{elapsed:r#}'; - - compiled = createCompiled(bench, decompilable, deferred, funcBody); - - try { - // Pretest one more time to check for errors. - bench.count = 1; - compiled.call(bench, context, timer); - bench.count = count; - delete clone.error; - } catch (e) { - bench.count = count; - if (!clone.error) { - clone.error = e || new Error(String(e)); - } - } - } - // If no errors run the full test loop. - if (!clone.error) { - compiled = bench.compiled = clone.compiled = createCompiled(bench, decompilable, deferred, funcBody); - result = compiled.call(deferred || bench, context, timer).elapsed; - } + return object + } - return result; - }; + /* ------------------------------------------------------------------------*/ - /* ----------------------------------------------------------------------*/ + /** + * Aborts the benchmark without recording times. + * + * @memberOf Benchmark + * @returns {Object} The benchmark instance. + */ + function abort() { + let event, + bench = this, + resetting = calledBy.reset + + if (bench.running) { + event = Event('abort') + bench.emit(event) + if (!event.cancelled || resetting) { + // Avoid infinite recursion. + calledBy.abort = true + bench.reset() + delete calledBy.abort + + if (support.timeout) { + clearTimeout(bench._timerId) + delete bench._timerId + } + if (!resetting) { + bench.aborted = true + bench.running = false + } + } + } - /** - * Creates a compiled function from the given function `body`. - */ - function createCompiled(bench, decompilable, deferred, body) { - let fn = bench.fn, - fnArg = deferred ? getFirstArgument(fn) || 'deferred' : ''; - - templateData.uid = uid + uidCounter++; - - _.assign(templateData, { - 'setup': decompilable ? getSource(bench.setup) : interpolate('m#.setup()'), - 'fn': decompilable ? getSource(fn) : interpolate('m#.fn(' + fnArg + ')'), - 'fnArg': fnArg, - 'teardown': decompilable ? getSource(bench.teardown) : interpolate('m#.teardown()') - }); - - // Use API of chosen timer. - if (timer.unit == 'ns') { - _.assign(templateData, { - 'begin': interpolate('s#=n#()'), - 'end': interpolate('r#=n#(s#);r#=r#[0]+(r#[1]/1e9)') - }); - } else if (timer.unit == 'us') { - if (timer.ns.stop) { - _.assign(templateData, { - 'begin': interpolate('s#=n#.start()'), - 'end': interpolate('r#=n#.microseconds()/1e6') - }); - } else { - _.assign(templateData, { - 'begin': interpolate('s#=n#()'), - 'end': interpolate('r#=(n#()-s#)/1e6') - }); - } - } else if (timer.ns.now) { - _.assign(templateData, { - 'begin': interpolate('s#=n#.now()'), - 'end': interpolate('r#=(n#.now()-s#)/1e3') - }); - } else { - _.assign(templateData, { - 'begin': interpolate('s#=new n#().getTime()'), - 'end': interpolate('r#=(new n#().getTime()-s#)/1e3') - }); - } - // Define `timer` methods. - timer.start = createFunction( - interpolate('o#'), - interpolate('var n#=this.ns,${begin};o#.elapsed=0;o#.timeStamp=s#') - ); - - timer.stop = createFunction( - interpolate('o#'), - interpolate('var n#=this.ns,s#=o#.timeStamp,${end};o#.elapsed=r#') - ); - - // Create compiled test. - return createFunction( - interpolate('window,t#'), - 'var global = window, clearTimeout = global.clearTimeout, setTimeout = global.setTimeout;\n' + - interpolate(body) - ); - } + return bench + } - /** - * Gets the current timer's minimum resolution (secs). - */ - function getRes(unit) { - let measured, - begin, - count = 30, - divisor = 1e3, - ns = timer.ns, - sample = []; - - // Get average smallest measurable time. - while (count--) { - if (unit == 'us') { - divisor = 1e6; - if (ns.stop) { - ns.start(); - while (!(measured = ns.microseconds())) {} - } else { - begin = ns(); - while (!(measured = ns() - begin)) {} - } - } else if (unit == 'ns') { - divisor = 1e9; - begin = (begin = ns())[0] + (begin[1] / divisor); - while (!(measured = ((measured = ns())[0] + (measured[1] / divisor)) - begin)) {} - divisor = 1; - } else if (ns.now) { - begin = ns.now(); - while (!(measured = ns.now() - begin)) {} - } else { - begin = new ns().getTime(); - while (!(measured = new ns().getTime() - begin)) {} - } - // Check for broken timers. - if (measured > 0) { - sample.push(measured); - } else { - sample.push(Infinity); - break; - } - } + /** + * Creates a new benchmark using the same test and options. + * + * @memberOf Benchmark + * @param {Object} options Options object to overwrite cloned options. + * @returns {Object} The new benchmark instance. + * @example + * + * var bizarro = bench.clone({ + * 'name': 'doppelganger' + * }); + */ + function clone(options) { + let bench = this, + result = new bench.constructor(_.assign({}, bench, options)) + + // Correct the `options` object. + result.options = _.assign( + {}, + cloneDeep(bench.options), + cloneDeep(options) + ) + + // Copy own custom properties. + _.forOwn(bench, (value, key) => { + if (!_.has(result, key)) { + result[key] = cloneDeep(value) + } + }) - // Convert to seconds. - return getMean(sample) / divisor; - } + return result + } - /** - * Interpolates a given template string. - */ - function interpolate(string) { - // Replaces all occurrences of `#` with a unique number and template tokens with content. - return _.template(string.replace(/\#/g, /\d+/.exec(templateData.uid)))(templateData); - } + /** + * Determines if a benchmark is faster than another. + * + * @memberOf Benchmark + * @param {Object} other The benchmark to compare. + * @returns {number} Returns `-1` if slower, `1` if faster, and `0` if indeterminate. + */ + function compare(other) { + const bench = this + + // Exit early if comparing the same benchmark. + if (bench == other) { + return 0 + } + let critical, + zStat, + sample1 = bench.stats.sample, + sample2 = other.stats.sample, + size1 = sample1.length, + size2 = sample2.length, + maxSize = max(size1, size2), + minSize = min(size1, size2), + u1 = getU(sample1, sample2), + u2 = getU(sample2, sample1), + u = min(u1, u2) + + function getScore(xA, sampleB) { + return _.reduce( + sampleB, + (total, xB) => total + (xB > xA ? 0 : xB < xA ? 1 : 0.5), + 0 + ) + } + + function getU(sampleA, sampleB) { + return _.reduce( + sampleA, + (total, xA) => total + getScore(xA, sampleB), + 0 + ) + } + + function getZ(u) { + return ( + (u - (size1 * size2) / 2) / + sqrt((size1 * size2 * (size1 + size2 + 1)) / 12) + ) + } + // Reject the null hypothesis the two samples come from the + // same population (i.e. have the same median) if... + if (size1 + size2 > 30) { + // ...the z-stat is greater than 1.96 or less than -1.96 + // http://www.statisticslectures.com/topics/mannwhitneyu/ + zStat = getZ(u) + + return abs(zStat) > 1.96 ? (u == u1 ? 1 : -1) : 0 + } + // ...the U value is less than or equal the critical U value. + critical = maxSize < 5 || minSize < 3 ? 0 : uTable[maxSize][minSize - 3] + + return u <= critical ? (u == u1 ? 1 : -1) : 0 + } - /* ----------------------------------------------------------------------*/ - - // Detect Chrome's microsecond timer: - // enable benchmarking via the --enable-benchmarking command - // line switch in at least Chrome 7 to use chrome.Interval - try { - if ((timer.ns = new (context.chrome || context.chromium).Interval())) { - timers.push({ - 'ns': timer.ns, - 'res': getRes('us'), - 'unit': 'us' - }); - } - } catch (e) {} - - // Detect Node.js's nanosecond resolution timer available in Node.js >= 0.8. - if (processObject && typeof (timer.ns = processObject.hrtime) == 'function') { - timers.push({ - 'ns': timer.ns, - 'res': getRes('ns'), - 'unit': 'ns' - }); + /** + * Reset properties and abort if running. + * + * @memberOf Benchmark + * @returns {Object} The benchmark instance. + */ + function reset() { + const bench = this + + if (bench.running && !calledBy.abort) { + // No worries, `reset()` is called within `abort()`. + calledBy.reset = true + bench.abort() + delete calledBy.reset + + return bench + } + let event, + index = 0, + changes = [], + queue = [] + + // A non-recursive solution to check if properties have changed. + // For more information see http://www.jslab.dk/articles/non.recursive.preorder.traversal.part4. + let data = { + destination: bench, + source: _.assign( + {}, + cloneDeep(bench.constructor.prototype), + cloneDeep(bench.options) + ), + } + + do { + _.forOwn(data.source, (value, key) => { + let changed, + destination = data.destination, + currValue = destination[key] + + // Skip pseudo private properties and event listeners. + if (/^_|^events$|^on[A-Z]/.test(key)) { + return + } + if (_.isObjectLike(value)) { + if (_.isArray(value)) { + // Check if an array value has changed to a non-array value. + if (!_.isArray(currValue)) { + changed = true + currValue = [] + } + // Check if an array has changed its length. + if (currValue.length != value.length) { + changed = true + currValue = currValue.slice(0, value.length) + currValue.length = value.length + } } - // Detect Wade Simmons' Node.js `microtime` module. - if (microtimeObject && typeof (timer.ns = microtimeObject.now) == 'function') { - timers.push({ - 'ns': timer.ns, - 'res': getRes('us'), - 'unit': 'us' - }); + // Check if an object has changed to a non-object value. + else if (!_.isObjectLike(currValue)) { + changed = true + currValue = {} } - // Pick timer with highest resolution. - timer = _.minBy(timers, 'res'); - - // Error if there are no working timers. - if (timer.res == Infinity) { - throw new Error('Benchmark.js was unable to find a working timer.'); + // Register a changed object. + if (changed) { + changes.push({ + destination: destination, + key: key, + value: currValue, + }) } - // Resolve time span required to achieve a percent uncertainty of at most 1%. - // For more information see http://spiff.rit.edu/classes/phys273/uncert/uncert.html. - options.minTime || (options.minTime = max(timer.res / 2 / 0.01, 0.05)); + queue.push({ + destination: currValue, + source: value, + }) + } + // Register a changed primitive. + else if (!_.eq(currValue, value) && value !== undefined) { + changes.push({ + destination: destination, + key: key, + value: value, + }) + } + }) + } while ((data = queue[index++])) + + // If changed emit the `reset` event and if it isn't cancelled reset the benchmark. + if ( + changes.length && + (bench.emit((event = Event('reset'))), !event.cancelled) + ) { + _.each(changes, (data) => { + data.destination[data.key] = data.value + }) + } + + return bench + } - return clock.apply(null, arguments); + /** + * Displays relevant benchmark information when coerced to a string. + * + * @name toString + * @memberOf Benchmark + * @returns {string} A string representation of the benchmark instance. + */ + function toStringBench() { + let bench = this, + error = bench.error, + hz = bench.hz, + id = bench.id, + stats = bench.stats, + size = stats.sample.length, + pm = '\xb1', + result = bench.name || (_.isNaN(id) ? id : '') + + if (error) { + let errorStr + + if (!_.isObject(error)) { + errorStr = String(error) + } else if (!_.isError(Error)) { + errorStr = join(error) + } else { + // Error#name and Error#message properties are non-enumerable. + errorStr = join( + _.assign( + { + name: error.name, + message: error.message, + }, + error + ) + ) } + result += ': ' + errorStr + } else { + result += + ' x ' + + formatNumber(hz.toFixed(hz < 100 ? 2 : 0)) + + ' ops/sec ' + + pm + + stats.rme.toFixed(2) + + '% (' + + size + + ' run' + + (size == 1 ? '' : 's') + + ' sampled)' + } + + return result + } - /* ------------------------------------------------------------------------*/ + /* ------------------------------------------------------------------------*/ - /** - * Computes stats on benchmark results. - * - * @private - * @param {Object} bench The benchmark instance. - * @param {Object} options The options object. - */ - function compute(bench, options) { - options || (options = {}); - - let async = options.async, - elapsed = 0, - initCount = bench.initCount, - minSamples = bench.minSamples, - queue = [], - sample = bench.stats.sample; - - /** - * Adds a clone to the queue. - */ - function enqueue() { - queue.push(_.assign(bench.clone(), { - '_original': bench, - 'events': { - 'abort': [update], - 'cycle': [update], - 'error': [update], - 'start': [update] - } - })); + /** + * Clocks the time taken to execute a test per cycle (secs). + * + * @private + * @param {Object} bench The benchmark instance. + * @returns {number} The time taken. + */ + function clock() { + let options = Benchmark.options, + templateData = {}, + timers = [ + { + ns: timer.ns, + res: max(0.0015, getRes('ms')), + unit: 'ms', + }, + ] + + // Lazy define for hi-res timers. + clock = function (clone) { + let deferred + + if (clone instanceof Deferred) { + deferred = clone + clone = deferred.benchmark + } + let bench = clone._original, + stringable = isStringable(bench.fn), + count = (bench.count = clone.count), + decompilable = + stringable || + (support.decompilation && + (clone.setup !== _.noop || clone.teardown !== _.noop)), + id = bench.id, + name = + bench.name || (typeof id == 'number' ? '' : id), + result = 0 + + // Init `minTime` if needed. + clone.minTime = + bench.minTime || + (bench.minTime = bench.options.minTime = options.minTime) + + // Compile in setup/teardown functions and the test loop. + // Create a new compiled test, instead of using the cached `bench.compiled`, + // to avoid potential engine optimizations enabled over the life of the test. + let funcBody = deferred + ? 'var d#=this,${fnArg}=d#,m#=d#.benchmark._original,f#=m#.fn,su#=m#.setup,td#=m#.teardown;' + + // When `deferred.cycles` is `0` then... + 'if(!d#.cycles){' + + // set `deferred.fn`, + 'd#.fn=function(){var ${fnArg}=d#;if(typeof f#=="function"){try{${fn}\n}catch(e#){f#(d#)}}else{${fn}\n}};' + + // set `deferred.teardown`, + 'd#.teardown=function(){d#.cycles=0;if(typeof td#=="function"){try{${teardown}\n}catch(e#){td#()}}else{${teardown}\n}};' + + // execute the benchmark's `setup`, + 'if(typeof su#=="function"){try{${setup}\n}catch(e#){su#()}}else{${setup}\n};' + + // start timer, + 't#.start(d#);' + + // and then execute `deferred.fn` and return a dummy object. + '}d#.fn();return{uid:"${uid}"}' + : 'var r#,s#,m#=this,f#=m#.fn,i#=m#.count,n#=t#.ns;${setup}\n${begin};' + + 'while(i#--){${fn}\n}${end};${teardown}\nreturn{elapsed:r#,uid:"${uid}"}' + + let compiled = (bench.compiled = clone.compiled = createCompiled( + bench, + decompilable, + deferred, + funcBody + )), + isEmpty = !(templateData.fn || stringable) + + try { + if (isEmpty) { + // Firefox may remove dead code from `Function#toString` results. + // For more information see http://bugzil.la/536085. + throw new Error( + 'The test "' + + name + + '" is empty. This may be the result of dead code removal.' + ) + } else if (!deferred) { + // Pretest to determine if compiled code exits early, usually by a + // rogue `return` statement, by checking for a return object with the uid. + bench.count = 1 + compiled = + decompilable && + (compiled.call(bench, context, timer) || {}).uid == + templateData.uid && + compiled + bench.count = count + } + } catch (e) { + compiled = null + clone.error = e || new Error(String(e)) + bench.count = count + } + // Fallback when a test exits early or errors during pretest. + if (!compiled && !deferred && !isEmpty) { + funcBody = + (stringable || (decompilable && !clone.error) + ? 'function f#(){${fn}\n}var r#,s#,m#=this,i#=m#.count' + : 'var r#,s#,m#=this,f#=m#.fn,i#=m#.count') + + ',n#=t#.ns;${setup}\n${begin};m#.f#=f#;while(i#--){m#.f#()}${end};' + + 'delete m#.f#;${teardown}\nreturn{elapsed:r#}' + + compiled = createCompiled(bench, decompilable, deferred, funcBody) + + try { + // Pretest one more time to check for errors. + bench.count = 1 + compiled.call(bench, context, timer) + bench.count = count + delete clone.error + } catch (e) { + bench.count = count + if (!clone.error) { + clone.error = e || new Error(String(e)) } + } + } + // If no errors run the full test loop. + if (!clone.error) { + compiled = bench.compiled = clone.compiled = createCompiled( + bench, + decompilable, + deferred, + funcBody + ) + result = compiled.call(deferred || bench, context, timer).elapsed + } - /** - * Updates the clone/original benchmarks to keep their data in sync. - */ - function update(event) { - let clone = this, - type = event.type; - - if (bench.running) { - if (type == 'start') { - // Note: `clone.minTime` prop is inited in `clock()`. - clone.count = bench.initCount; - } else { - if (type == 'error') { - bench.error = clone.error; - } - if (type == 'abort') { - bench.abort(); - bench.emit('cycle'); - } else { - event.currentTarget = event.target = bench; - bench.emit(event); - } - } - } else if (bench.aborted) { - // Clear abort listeners to avoid triggering bench's abort/cycle again. - clone.events.abort.length = 0; - clone.abort(); - } - } + return result + } - /** - * Determines if more clones should be queued or if cycling should stop. - */ - function evaluate(event) { - let critical, - df, - mean, - moe, - rme, - sd, - sem, - variance, - clone = event.target, - done = bench.aborted, - now = _.now(), - size = sample.push(clone.times.period), - maxedOut = size >= minSamples && (elapsed += now - clone.times.timeStamp) / 1e3 > bench.maxTime, - times = bench.times, - varOf = function(sum, x) { - return sum + pow(x - mean, 2); - }; - - // Exit early for aborted or unclockable tests. - if (done || clone.hz == Infinity) { - maxedOut = !(size = sample.length = queue.length = 0); - } + /* ----------------------------------------------------------------------*/ - if (!done) { - // Compute the sample mean (estimate of the population mean). - mean = getMean(sample); - // Compute the sample variance (estimate of the population variance). - variance = _.reduce(sample, varOf, 0) / (size - 1) || 0; - // Compute the sample standard deviation (estimate of the population standard deviation). - sd = sqrt(variance); - // Compute the standard error of the mean (a.k.a. the standard deviation of the sampling distribution of the sample mean). - sem = sd / sqrt(size); - // Compute the degrees of freedom. - df = size - 1; - // Compute the critical value. - critical = tTable[Math.round(df) || 1] || tTable.infinity; - // Compute the margin of error. - moe = sem * critical; - // Compute the relative margin of error. - rme = (moe / mean) * 100 || 0; - - _.assign(bench.stats, { - 'deviation': sd, - 'mean': mean, - 'moe': moe, - 'rme': rme, - 'sem': sem, - 'variance': variance - }); - - // Abort the cycle loop when the minimum sample size has been collected - // and the elapsed time exceeds the maximum time allowed per benchmark. - // We don't count cycle delays toward the max time because delays may be - // increased by browsers that clamp timeouts for inactive tabs. For more - // information see https://developer.mozilla.org/en/window.setTimeout#Inactive_tabs. - if (maxedOut) { - // Reset the `initCount` in case the benchmark is rerun. - bench.initCount = initCount; - bench.running = false; - done = true; - times.elapsed = (now - times.timeStamp) / 1e3; - } - if (bench.hz != Infinity) { - bench.hz = 1 / mean; - times.cycle = mean * bench.count; - times.period = mean; - } - } - // If time permits, increase sample size to reduce the margin of error. - if (queue.length < 2 && !maxedOut) { - enqueue(); - } - // Abort the `invoke` cycle when done. - event.aborted = done; + /** + * Creates a compiled function from the given function `body`. + */ + function createCompiled(bench, decompilable, deferred, body) { + let fn = bench.fn, + fnArg = deferred ? getFirstArgument(fn) || 'deferred' : '' + + templateData.uid = uid + uidCounter++ + + _.assign(templateData, { + setup: decompilable + ? getSource(bench.setup) + : interpolate('m#.setup()'), + fn: decompilable + ? getSource(fn) + : interpolate('m#.fn(' + fnArg + ')'), + fnArg: fnArg, + teardown: decompilable + ? getSource(bench.teardown) + : interpolate('m#.teardown()'), + }) + + // Use API of chosen timer. + if (timer.unit == 'ns') { + _.assign(templateData, { + begin: interpolate('s#=n#()'), + end: interpolate('r#=n#(s#);r#=r#[0]+(r#[1]/1e9)'), + }) + } else if (timer.unit == 'us') { + if (timer.ns.stop) { + _.assign(templateData, { + begin: interpolate('s#=n#.start()'), + end: interpolate('r#=n#.microseconds()/1e6'), + }) + } else { + _.assign(templateData, { + begin: interpolate('s#=n#()'), + end: interpolate('r#=(n#()-s#)/1e6'), + }) + } + } else if (timer.ns.now) { + _.assign(templateData, { + begin: interpolate('s#=n#.now()'), + end: interpolate('r#=(n#.now()-s#)/1e3'), + }) + } else { + _.assign(templateData, { + begin: interpolate('s#=new n#().getTime()'), + end: interpolate('r#=(new n#().getTime()-s#)/1e3'), + }) + } + // Define `timer` methods. + timer.start = createFunction( + interpolate('o#'), + interpolate('var n#=this.ns,${begin};o#.elapsed=0;o#.timeStamp=s#') + ) + + timer.stop = createFunction( + interpolate('o#'), + interpolate('var n#=this.ns,s#=o#.timeStamp,${end};o#.elapsed=r#') + ) + + // Create compiled test. + return createFunction( + interpolate('window,t#'), + 'var global = window, clearTimeout = global.clearTimeout, setTimeout = global.setTimeout;\n' + + interpolate(body) + ) + } + + /** + * Gets the current timer's minimum resolution (secs). + */ + function getRes(unit) { + let measured, + begin, + count = 30, + divisor = 1e3, + ns = timer.ns, + sample = [] + + // Get average smallest measurable time. + while (count--) { + if (unit == 'us') { + divisor = 1e6 + if (ns.stop) { + ns.start() + while (!(measured = ns.microseconds())) {} + } else { + begin = ns() + while (!(measured = ns() - begin)) {} } + } else if (unit == 'ns') { + divisor = 1e9 + begin = (begin = ns())[0] + begin[1] / divisor + while ( + !(measured = (measured = ns())[0] + measured[1] / divisor - begin) + ) {} + divisor = 1 + } else if (ns.now) { + begin = ns.now() + while (!(measured = ns.now() - begin)) {} + } else { + begin = new ns().getTime() + while (!(measured = new ns().getTime() - begin)) {} + } + // Check for broken timers. + if (measured > 0) { + sample.push(measured) + } else { + sample.push(Infinity) + break + } + } - // Init queue and begin. - enqueue(); - invoke(queue, { - 'name': 'run', - 'args': { 'async': async }, - 'queued': true, - 'onCycle': evaluate, - 'onComplete': function() { - bench.emit('complete'); - } - }); + // Convert to seconds. + return getMean(sample) / divisor + } + + /** + * Interpolates a given template string. + */ + function interpolate(string) { + // Replaces all occurrences of `#` with a unique number and template tokens with content. + return _.template(string.replace(/\#/g, /\d+/.exec(templateData.uid)))( + templateData + ) + } + + /* ----------------------------------------------------------------------*/ + + // Detect Chrome's microsecond timer: + // enable benchmarking via the --enable-benchmarking command + // line switch in at least Chrome 7 to use chrome.Interval + try { + if ((timer.ns = new (context.chrome || context.chromium).Interval())) { + timers.push({ + ns: timer.ns, + res: getRes('us'), + unit: 'us', + }) } + } catch (e) {} + + // Detect Node.js's nanosecond resolution timer available in Node.js >= 0.8. + if ( + processObject && + typeof (timer.ns = processObject.hrtime) == 'function' + ) { + timers.push({ + ns: timer.ns, + res: getRes('ns'), + unit: 'ns', + }) + } + // Detect Wade Simmons' Node.js `microtime` module. + if ( + microtimeObject && + typeof (timer.ns = microtimeObject.now) == 'function' + ) { + timers.push({ + ns: timer.ns, + res: getRes('us'), + unit: 'us', + }) + } + // Pick timer with highest resolution. + timer = _.minBy(timers, 'res') + + // Error if there are no working timers. + if (timer.res == Infinity) { + throw new Error('Benchmark.js was unable to find a working timer.') + } + // Resolve time span required to achieve a percent uncertainty of at most 1%. + // For more information see http://spiff.rit.edu/classes/phys273/uncert/uncert.html. + options.minTime || (options.minTime = max(timer.res / 2 / 0.01, 0.05)) + + return clock.apply(null, arguments) + } - /* ------------------------------------------------------------------------*/ + /* ------------------------------------------------------------------------*/ - /** - * Cycles a benchmark until a run `count` can be established. - * - * @private - * @param {Object} clone The cloned benchmark instance. - * @param {Object} options The options object. + /** + * Computes stats on benchmark results. + * + * @private + * @param {Object} bench The benchmark instance. + * @param {Object} options The options object. + */ + function compute(bench, options) { + options || (options = {}) + + let async = options.async, + elapsed = 0, + initCount = bench.initCount, + minSamples = bench.minSamples, + queue = [], + sample = bench.stats.sample + + /** + * Adds a clone to the queue. */ - function cycle(clone, options) { - options || (options = {}); - - let deferred; + function enqueue() { + queue.push( + _.assign(bench.clone(), { + _original: bench, + events: { + abort: [update], + cycle: [update], + error: [update], + start: [update], + }, + }) + ) + } - if (clone instanceof Deferred) { - deferred = clone; - clone = clone.benchmark; - } - let clocked, - cycles, - divisor, - event, - minTime, - period, - async = options.async, - bench = clone._original, - count = clone.count, - times = clone.times; - - // Continue, if not aborted between cycles. - if (clone.running) { - // `minTime` is set to `Benchmark.options.minTime` in `clock()`. - cycles = ++clone.cycles; - clocked = deferred ? deferred.elapsed : clock(clone); - minTime = clone.minTime; - - if (cycles > bench.cycles) { - bench.cycles = cycles; - } - if (clone.error) { - event = Event('error'); - event.message = clone.error; - clone.emit(event); - if (!event.cancelled) { - clone.abort(); - } - } - } - // Continue, if not errored. - if (clone.running) { - // Compute the time taken to complete last test cycle. - bench.times.cycle = times.cycle = clocked; - // Compute the seconds per operation. - period = bench.times.period = times.period = clocked / count; - // Compute the ops per second. - bench.hz = clone.hz = 1 / period; - // Avoid working our way up to this next time. - bench.initCount = clone.initCount = count; - // Do we need to do another cycle? - clone.running = clocked < minTime; - - if (clone.running) { - // Tests may clock at `0` when `initCount` is a small number, - // to avoid that we set its count to something a bit higher. - if (!clocked && (divisor = divisors[clone.cycles]) != null) { - count = floor(4e6 / divisor); - } - // Calculate how many more iterations it will take to achieve the `minTime`. - if (count <= clone.count) { - count += Math.ceil((minTime - clocked) / period); - } - clone.running = count != Infinity; - } - } - // Should we exit early? - event = Event('cycle'); - clone.emit(event); - if (event.aborted) { - clone.abort(); + /** + * Updates the clone/original benchmarks to keep their data in sync. + */ + function update(event) { + let clone = this, + type = event.type + + if (bench.running) { + if (type == 'start') { + // Note: `clone.minTime` prop is inited in `clock()`. + clone.count = bench.initCount + } else { + if (type == 'error') { + bench.error = clone.error } - // Figure out what to do next. - if (clone.running) { - // Start a new cycle. - clone.count = count; - if (deferred) { - clone.compiled.call(deferred, context, timer); - } else if (async) { - delay(clone, () => { - cycle(clone, options); - }); - } else { - cycle(clone); - } + if (type == 'abort') { + bench.abort() + bench.emit('cycle') } else { - // Fix TraceMonkey bug associated with clock fallbacks. - // For more information see http://bugzil.la/509069. - if (support.browser) { - runScript(uid + '=1;delete ' + uid); - } - // We're done. - clone.emit('complete'); + event.currentTarget = event.target = bench + bench.emit(event) } + } + } else if (bench.aborted) { + // Clear abort listeners to avoid triggering bench's abort/cycle again. + clone.events.abort.length = 0 + clone.abort() } + } - /* ------------------------------------------------------------------------*/ - - /** - * Runs the benchmark. - * - * @memberOf Benchmark - * @param {Object} [options={}] Options object. - * @returns {Object} The benchmark instance. - * @example - * - * // basic usage - * bench.run(); - * - * // or with options - * bench.run({ 'async': true }); + /** + * Determines if more clones should be queued or if cycling should stop. */ - function run(options) { - let bench = this, - event = Event('start'); - - // Set `running` to `false` so `reset()` won't call `abort()`. - bench.running = false; - bench.reset(); - bench.running = true; - - bench.count = bench.initCount; - bench.times.timeStamp = _.now(); - bench.emit(event); - - if (!event.cancelled) { - options = { 'async': ((options = options && options.async) == null ? bench.async : options) && support.timeout }; - - // For clones created within `compute()`. - if (bench._original) { - if (bench.defer) { - Deferred(bench); - } else { - cycle(bench, options); - } - } - // For original benchmarks. - else { - compute(bench, options); - } - } + function evaluate(event) { + let critical, + df, + mean, + moe, + rme, + sd, + sem, + variance, + clone = event.target, + done = bench.aborted, + now = _.now(), + size = sample.push(clone.times.period), + maxedOut = + size >= minSamples && + (elapsed += now - clone.times.timeStamp) / 1e3 > bench.maxTime, + times = bench.times, + varOf = function (sum, x) { + return sum + pow(x - mean, 2) + } + + // Exit early for aborted or unclockable tests. + if (done || clone.hz == Infinity) { + maxedOut = !(size = sample.length = queue.length = 0) + } + + if (!done) { + // Compute the sample mean (estimate of the population mean). + mean = getMean(sample) + // Compute the sample variance (estimate of the population variance). + variance = _.reduce(sample, varOf, 0) / (size - 1) || 0 + // Compute the sample standard deviation (estimate of the population standard deviation). + sd = sqrt(variance) + // Compute the standard error of the mean (a.k.a. the standard deviation of the sampling distribution of the sample mean). + sem = sd / sqrt(size) + // Compute the degrees of freedom. + df = size - 1 + // Compute the critical value. + critical = tTable[Math.round(df) || 1] || tTable.infinity + // Compute the margin of error. + moe = sem * critical + // Compute the relative margin of error. + rme = (moe / mean) * 100 || 0 + + _.assign(bench.stats, { + deviation: sd, + mean: mean, + moe: moe, + rme: rme, + sem: sem, + variance: variance, + }) + + // Abort the cycle loop when the minimum sample size has been collected + // and the elapsed time exceeds the maximum time allowed per benchmark. + // We don't count cycle delays toward the max time because delays may be + // increased by browsers that clamp timeouts for inactive tabs. For more + // information see https://developer.mozilla.org/en/window.setTimeout#Inactive_tabs. + if (maxedOut) { + // Reset the `initCount` in case the benchmark is rerun. + bench.initCount = initCount + bench.running = false + done = true + times.elapsed = (now - times.timeStamp) / 1e3 + } + if (bench.hz != Infinity) { + bench.hz = 1 / mean + times.cycle = mean * bench.count + times.period = mean + } + } + // If time permits, increase sample size to reduce the margin of error. + if (queue.length < 2 && !maxedOut) { + enqueue() + } + // Abort the `invoke` cycle when done. + event.aborted = done + } + + // Init queue and begin. + enqueue() + invoke(queue, { + name: 'run', + args: { async: async }, + queued: true, + onCycle: evaluate, + onComplete: function () { + bench.emit('complete') + }, + }) + } + + /* ------------------------------------------------------------------------*/ + + /** + * Cycles a benchmark until a run `count` can be established. + * + * @private + * @param {Object} clone The cloned benchmark instance. + * @param {Object} options The options object. + */ + function cycle(clone, options) { + options || (options = {}) + + let deferred + + if (clone instanceof Deferred) { + deferred = clone + clone = clone.benchmark + } + let clocked, + cycles, + divisor, + event, + minTime, + period, + async = options.async, + bench = clone._original, + count = clone.count, + times = clone.times + + // Continue, if not aborted between cycles. + if (clone.running) { + // `minTime` is set to `Benchmark.options.minTime` in `clock()`. + cycles = ++clone.cycles + clocked = deferred ? deferred.elapsed : clock(clone) + minTime = clone.minTime + + if (cycles > bench.cycles) { + bench.cycles = cycles + } + if (clone.error) { + event = Event('error') + event.message = clone.error + clone.emit(event) + if (!event.cancelled) { + clone.abort() + } + } + } + // Continue, if not errored. + if (clone.running) { + // Compute the time taken to complete last test cycle. + bench.times.cycle = times.cycle = clocked + // Compute the seconds per operation. + period = bench.times.period = times.period = clocked / count + // Compute the ops per second. + bench.hz = clone.hz = 1 / period + // Avoid working our way up to this next time. + bench.initCount = clone.initCount = count + // Do we need to do another cycle? + clone.running = clocked < minTime + + if (clone.running) { + // Tests may clock at `0` when `initCount` is a small number, + // to avoid that we set its count to something a bit higher. + if (!clocked && (divisor = divisors[clone.cycles]) != null) { + count = floor(4e6 / divisor) + } + // Calculate how many more iterations it will take to achieve the `minTime`. + if (count <= clone.count) { + count += Math.ceil((minTime - clocked) / period) + } + clone.running = count != Infinity + } + } + // Should we exit early? + event = Event('cycle') + clone.emit(event) + if (event.aborted) { + clone.abort() + } + // Figure out what to do next. + if (clone.running) { + // Start a new cycle. + clone.count = count + if (deferred) { + clone.compiled.call(deferred, context, timer) + } else if (async) { + delay(clone, () => { + cycle(clone, options) + }) + } else { + cycle(clone) + } + } else { + // Fix TraceMonkey bug associated with clock fallbacks. + // For more information see http://bugzil.la/509069. + if (support.browser) { + runScript(uid + '=1;delete ' + uid) + } + // We're done. + clone.emit('complete') + } + } + + /* ------------------------------------------------------------------------*/ - return bench; + /** + * Runs the benchmark. + * + * @memberOf Benchmark + * @param {Object} [options={}] Options object. + * @returns {Object} The benchmark instance. + * @example + * + * // basic usage + * bench.run(); + * + * // or with options + * bench.run({ 'async': true }); + */ + function run(options) { + let bench = this, + event = Event('start') + + // Set `running` to `false` so `reset()` won't call `abort()`. + bench.running = false + bench.reset() + bench.running = true + + bench.count = bench.initCount + bench.times.timeStamp = _.now() + bench.emit(event) + + if (!event.cancelled) { + options = { + async: + ((options = options && options.async) == null + ? bench.async + : options) && support.timeout, + } + + // For clones created within `compute()`. + if (bench._original) { + if (bench.defer) { + Deferred(bench) + } else { + cycle(bench, options) + } } + // For original benchmarks. + else { + compute(bench, options) + } + } - /* ------------------------------------------------------------------------*/ + return bench + } - // Firefox 1 erroneously defines variable and argument names of functions on - // the function itself as non-configurable properties with `undefined` values. - // The bugginess continues as the `Benchmark` constructor has an argument - // named `options` and Firefox 1 will not assign a value to `Benchmark.options`, - // making it non-writable in the process, unless it is the first property - // assigned by for-in loop of `_.assign()`. - _.assign(Benchmark, { + /* ------------------------------------------------------------------------*/ + // Firefox 1 erroneously defines variable and argument names of functions on + // the function itself as non-configurable properties with `undefined` values. + // The bugginess continues as the `Benchmark` constructor has an argument + // named `options` and Firefox 1 will not assign a value to `Benchmark.options`, + // making it non-writable in the process, unless it is the first property + // assigned by for-in loop of `_.assign()`. + _.assign(Benchmark, { + /** + * The default options copied by benchmark instances. + * + * @static + * @memberOf Benchmark + * @type Object + */ + options: { /** - * The default options copied by benchmark instances. + * A flag to indicate that benchmark cycles will execute asynchronously + * by default. * - * @static - * @memberOf Benchmark - * @type Object + * @memberOf Benchmark.options + * @type boolean */ - 'options': { - - /** - * A flag to indicate that benchmark cycles will execute asynchronously - * by default. - * - * @memberOf Benchmark.options - * @type boolean - */ - 'async': false, - - /** - * A flag to indicate that the benchmark clock is deferred. - * - * @memberOf Benchmark.options - * @type boolean - */ - 'defer': false, - - /** - * The delay between test cycles (secs). - * @memberOf Benchmark.options - * @type number - */ - 'delay': 0.005, - - /** - * Displayed by `Benchmark#toString` when a `name` is not available - * (auto-generated if absent). - * - * @memberOf Benchmark.options - * @type string - */ - 'id': undefined, - - /** - * The default number of times to execute a test on a benchmark's first cycle. - * - * @memberOf Benchmark.options - * @type number - */ - 'initCount': 1, - - /** - * The maximum time a benchmark is allowed to run before finishing (secs). - * - * Note: Cycle delays aren't counted toward the maximum time. - * - * @memberOf Benchmark.options - * @type number - */ - 'maxTime': 5, - - /** - * The minimum sample size required to perform statistical analysis. - * - * @memberOf Benchmark.options - * @type number - */ - 'minSamples': 5, - - /** - * The time needed to reduce the percent uncertainty of measurement to 1% (secs). - * - * @memberOf Benchmark.options - * @type number - */ - 'minTime': 0, - - /** - * The name of the benchmark. - * - * @memberOf Benchmark.options - * @type string - */ - 'name': undefined, - - /** - * An event listener called when the benchmark is aborted. - * - * @memberOf Benchmark.options - * @type Function - */ - 'onAbort': undefined, - - /** - * An event listener called when the benchmark completes running. - * - * @memberOf Benchmark.options - * @type Function - */ - 'onComplete': undefined, - - /** - * An event listener called after each run cycle. - * - * @memberOf Benchmark.options - * @type Function - */ - 'onCycle': undefined, - - /** - * An event listener called when a test errors. - * - * @memberOf Benchmark.options - * @type Function - */ - 'onError': undefined, - - /** - * An event listener called when the benchmark is reset. - * - * @memberOf Benchmark.options - * @type Function - */ - 'onReset': undefined, - - /** - * An event listener called when the benchmark starts running. - * - * @memberOf Benchmark.options - * @type Function - */ - 'onStart': undefined - }, + async: false, - /** - * Platform object with properties describing things like browser name, - * version, and operating system. See [`platform.js`](https://mths.be/platform). + /** + * A flag to indicate that the benchmark clock is deferred. * - * @static - * @memberOf Benchmark - * @type Object + * @memberOf Benchmark.options + * @type boolean */ - 'platform': context.platform || require('platform') || ({ - 'description': context.navigator && context.navigator.userAgent || null, - 'layout': null, - 'product': null, - 'name': null, - 'manufacturer': null, - 'os': null, - 'prerelease': null, - 'version': null, - 'toString': function() { - return this.description || ''; - } - }), + defer: false, + + /** + * The delay between test cycles (secs). + * @memberOf Benchmark.options + * @type number + */ + delay: 0.005, - /** - * The semantic version number. + /** + * Displayed by `Benchmark#toString` when a `name` is not available + * (auto-generated if absent). * - * @static - * @memberOf Benchmark + * @memberOf Benchmark.options * @type string */ - 'version': '2.1.4' - }); - - _.assign(Benchmark, { - 'filter': filter, - 'formatNumber': formatNumber, - 'invoke': invoke, - 'join': join, - 'runInContext': runInContext, - 'support': support - }); - - // Add lodash methods to Benchmark. - _.each(['each', 'forEach', 'forOwn', 'has', 'indexOf', 'map', 'reduce'], (methodName) => { - Benchmark[methodName] = _[methodName]; - }); - - /* ------------------------------------------------------------------------*/ - - _.assign(Benchmark.prototype, { + id: undefined, /** - * The number of times a test was executed. + * The default number of times to execute a test on a benchmark's first cycle. * - * @memberOf Benchmark + * @memberOf Benchmark.options * @type number */ - 'count': 0, + initCount: 1, - /** - * The number of cycles performed while benchmarking. + /** + * The maximum time a benchmark is allowed to run before finishing (secs). * - * @memberOf Benchmark + * Note: Cycle delays aren't counted toward the maximum time. + * + * @memberOf Benchmark.options * @type number */ - 'cycles': 0, + maxTime: 5, - /** - * The number of executions per second. + /** + * The minimum sample size required to perform statistical analysis. * - * @memberOf Benchmark + * @memberOf Benchmark.options * @type number */ - 'hz': 0, + minSamples: 5, - /** - * The compiled test function. + /** + * The time needed to reduce the percent uncertainty of measurement to 1% (secs). * - * @memberOf Benchmark - * @type {Function|string} + * @memberOf Benchmark.options + * @type number */ - 'compiled': undefined, + minTime: 0, - /** - * The error object if the test failed. + /** + * The name of the benchmark. * - * @memberOf Benchmark - * @type Object + * @memberOf Benchmark.options + * @type string */ - 'error': undefined, + name: undefined, - /** - * The test to benchmark. + /** + * An event listener called when the benchmark is aborted. * - * @memberOf Benchmark - * @type {Function|string} + * @memberOf Benchmark.options + * @type Function */ - 'fn': undefined, + onAbort: undefined, - /** - * A flag to indicate if the benchmark is aborted. + /** + * An event listener called when the benchmark completes running. * - * @memberOf Benchmark - * @type boolean + * @memberOf Benchmark.options + * @type Function */ - 'aborted': false, + onComplete: undefined, - /** - * A flag to indicate if the benchmark is running. + /** + * An event listener called after each run cycle. * - * @memberOf Benchmark - * @type boolean + * @memberOf Benchmark.options + * @type Function */ - 'running': false, + onCycle: undefined, - /** - * Compiled into the test and executed immediately **before** the test loop. - * - * @memberOf Benchmark - * @type {Function|string} - * @example - * - * // basic usage - * var bench = Benchmark({ - * 'setup': function() { - * var c = this.count, - * element = document.getElementById('container'); - * while (c--) { - * element.appendChild(document.createElement('div')); - * } - * }, - * 'fn': function() { - * element.removeChild(element.lastChild); - * } - * }); - * - * // compiles to something like: - * var c = this.count, - * element = document.getElementById('container'); - * while (c--) { - * element.appendChild(document.createElement('div')); - * } - * var start = new Date; - * while (count--) { - * element.removeChild(element.lastChild); - * } - * var end = new Date - start; - * - * // or using strings - * var bench = Benchmark({ - * 'setup': '\ - * var a = 0;\n\ - * (function() {\n\ - * (function() {\n\ - * (function() {', - * 'fn': 'a += 1;', - * 'teardown': '\ - * }())\n\ - * }())\n\ - * }())' - * }); + /** + * An event listener called when a test errors. * - * // compiles to something like: - * var a = 0; - * (function() { - * (function() { - * (function() { - * var start = new Date; - * while (count--) { - * a += 1; - * } - * var end = new Date - start; - * }()) - * }()) - * }()) + * @memberOf Benchmark.options + * @type Function */ - 'setup': _.noop, + onError: undefined, - /** - * Compiled into the test and executed immediately **after** the test loop. + /** + * An event listener called when the benchmark is reset. * - * @memberOf Benchmark - * @type {Function|string} + * @memberOf Benchmark.options + * @type Function */ - 'teardown': _.noop, + onReset: undefined, - /** - * An object of stats including mean, margin or error, and standard deviation. + /** + * An event listener called when the benchmark starts running. * - * @memberOf Benchmark - * @type Object + * @memberOf Benchmark.options + * @type Function */ - 'stats': { - - /** - * The margin of error. - * - * @memberOf Benchmark#stats - * @type number - */ - 'moe': 0, - - /** - * The relative margin of error (expressed as a percentage of the mean). - * - * @memberOf Benchmark#stats - * @type number - */ - 'rme': 0, - - /** - * The standard error of the mean. - * - * @memberOf Benchmark#stats - * @type number - */ - 'sem': 0, - - /** - * The sample standard deviation. - * - * @memberOf Benchmark#stats - * @type number - */ - 'deviation': 0, - - /** - * The sample arithmetic mean (secs). - * - * @memberOf Benchmark#stats - * @type number - */ - 'mean': 0, - - /** - * The array of sampled periods. - * - * @memberOf Benchmark#stats - * @type Array - */ - 'sample': [], - - /** - * The sample variance. - * - * @memberOf Benchmark#stats - * @type number - */ - 'variance': 0 - }, + onStart: undefined, + }, - /** - * An object of timing data including cycle, elapsed, period, start, and stop. - * - * @memberOf Benchmark - * @type Object - */ - 'times': { - - /** - * The time taken to complete the last cycle (secs). - * - * @memberOf Benchmark#times - * @type number - */ - 'cycle': 0, - - /** - * The time taken to complete the benchmark (secs). - * - * @memberOf Benchmark#times - * @type number - */ - 'elapsed': 0, - - /** - * The time taken to execute the test once (secs). - * - * @memberOf Benchmark#times - * @type number - */ - 'period': 0, - - /** - * A timestamp of when the benchmark started (ms). - * - * @memberOf Benchmark#times - * @type number - */ - 'timeStamp': 0 - } - }); + /** + * Platform object with properties describing things like browser name, + * version, and operating system. See [`platform.js`](https://mths.be/platform). + * + * @static + * @memberOf Benchmark + * @type Object + */ + platform: context.platform || + require('platform') || { + description: + (context.navigator && context.navigator.userAgent) || null, + layout: null, + product: null, + name: null, + manufacturer: null, + os: null, + prerelease: null, + version: null, + toString: function () { + return this.description || '' + }, + }, + + /** + * The semantic version number. + * + * @static + * @memberOf Benchmark + * @type string + */ + version: '2.1.4', + }) + + _.assign(Benchmark, { + filter: filter, + formatNumber: formatNumber, + invoke: invoke, + join: join, + runInContext: runInContext, + support: support, + }) + + // Add lodash methods to Benchmark. + _.each( + ['each', 'forEach', 'forOwn', 'has', 'indexOf', 'map', 'reduce'], + (methodName) => { + Benchmark[methodName] = _[methodName] + } + ) + + /* ------------------------------------------------------------------------*/ + + _.assign(Benchmark.prototype, { + /** + * The number of times a test was executed. + * + * @memberOf Benchmark + * @type number + */ + count: 0, + + /** + * The number of cycles performed while benchmarking. + * + * @memberOf Benchmark + * @type number + */ + cycles: 0, + + /** + * The number of executions per second. + * + * @memberOf Benchmark + * @type number + */ + hz: 0, + + /** + * The compiled test function. + * + * @memberOf Benchmark + * @type {Function|string} + */ + compiled: undefined, + + /** + * The error object if the test failed. + * + * @memberOf Benchmark + * @type Object + */ + error: undefined, + + /** + * The test to benchmark. + * + * @memberOf Benchmark + * @type {Function|string} + */ + fn: undefined, + + /** + * A flag to indicate if the benchmark is aborted. + * + * @memberOf Benchmark + * @type boolean + */ + aborted: false, - _.assign(Benchmark.prototype, { - 'abort': abort, - 'clone': clone, - 'compare': compare, - 'emit': emit, - 'listeners': listeners, - 'off': off, - 'on': on, - 'reset': reset, - 'run': run, - 'toString': toStringBench - }); + /** + * A flag to indicate if the benchmark is running. + * + * @memberOf Benchmark + * @type boolean + */ + running: false, - /* ------------------------------------------------------------------------*/ + /** + * Compiled into the test and executed immediately **before** the test loop. + * + * @memberOf Benchmark + * @type {Function|string} + * @example + * + * // basic usage + * var bench = Benchmark({ + * 'setup': function() { + * var c = this.count, + * element = document.getElementById('container'); + * while (c--) { + * element.appendChild(document.createElement('div')); + * } + * }, + * 'fn': function() { + * element.removeChild(element.lastChild); + * } + * }); + * + * // compiles to something like: + * var c = this.count, + * element = document.getElementById('container'); + * while (c--) { + * element.appendChild(document.createElement('div')); + * } + * var start = new Date; + * while (count--) { + * element.removeChild(element.lastChild); + * } + * var end = new Date - start; + * + * // or using strings + * var bench = Benchmark({ + * 'setup': '\ + * var a = 0;\n\ + * (function() {\n\ + * (function() {\n\ + * (function() {', + * 'fn': 'a += 1;', + * 'teardown': '\ + * }())\n\ + * }())\n\ + * }())' + * }); + * + * // compiles to something like: + * var a = 0; + * (function() { + * (function() { + * (function() { + * var start = new Date; + * while (count--) { + * a += 1; + * } + * var end = new Date - start; + * }()) + * }()) + * }()) + */ + setup: _.noop, - _.assign(Deferred.prototype, { + /** + * Compiled into the test and executed immediately **after** the test loop. + * + * @memberOf Benchmark + * @type {Function|string} + */ + teardown: _.noop, + /** + * An object of stats including mean, margin or error, and standard deviation. + * + * @memberOf Benchmark + * @type Object + */ + stats: { /** - * The deferred benchmark instance. + * The margin of error. * - * @memberOf Benchmark.Deferred - * @type Object + * @memberOf Benchmark#stats + * @type number */ - 'benchmark': null, + moe: 0, - /** - * The number of deferred cycles performed while benchmarking. + /** + * The relative margin of error (expressed as a percentage of the mean). * - * @memberOf Benchmark.Deferred + * @memberOf Benchmark#stats * @type number */ - 'cycles': 0, + rme: 0, - /** - * The time taken to complete the deferred benchmark (secs). + /** + * The standard error of the mean. * - * @memberOf Benchmark.Deferred + * @memberOf Benchmark#stats * @type number */ - 'elapsed': 0, + sem: 0, - /** - * A timestamp of when the deferred benchmark started (ms). + /** + * The sample standard deviation. * - * @memberOf Benchmark.Deferred + * @memberOf Benchmark#stats * @type number */ - 'timeStamp': 0 - }); - - _.assign(Deferred.prototype, { 'resolve': resolve }); - - /* ------------------------------------------------------------------------*/ - - _.assign(Event.prototype, { + deviation: 0, /** - * A flag to indicate if the emitters listener iteration is aborted. + * The sample arithmetic mean (secs). * - * @memberOf Benchmark.Event - * @type boolean + * @memberOf Benchmark#stats + * @type number */ - 'aborted': false, + mean: 0, - /** - * A flag to indicate if the default action is cancelled. + /** + * The array of sampled periods. * - * @memberOf Benchmark.Event - * @type boolean + * @memberOf Benchmark#stats + * @type Array */ - 'cancelled': false, + sample: [], - /** - * The object whose listeners are currently being processed. + /** + * The sample variance. * - * @memberOf Benchmark.Event - * @type Object + * @memberOf Benchmark#stats + * @type number */ - 'currentTarget': undefined, + variance: 0, + }, - /** - * The return value of the last executed listener. + /** + * An object of timing data including cycle, elapsed, period, start, and stop. + * + * @memberOf Benchmark + * @type Object + */ + times: { + /** + * The time taken to complete the last cycle (secs). * - * @memberOf Benchmark.Event - * @type Mixed + * @memberOf Benchmark#times + * @type number */ - 'result': undefined, + cycle: 0, - /** - * The object to which the event was originally emitted. + /** + * The time taken to complete the benchmark (secs). * - * @memberOf Benchmark.Event - * @type Object + * @memberOf Benchmark#times + * @type number */ - 'target': undefined, + elapsed: 0, - /** - * A timestamp of when the event was created (ms). + /** + * The time taken to execute the test once (secs). * - * @memberOf Benchmark.Event + * @memberOf Benchmark#times * @type number */ - 'timeStamp': 0, + period: 0, - /** - * The event type. + /** + * A timestamp of when the benchmark started (ms). * - * @memberOf Benchmark.Event - * @type string + * @memberOf Benchmark#times + * @type number */ - 'type': '' - }); + timeStamp: 0, + }, + }) + + _.assign(Benchmark.prototype, { + abort: abort, + clone: clone, + compare: compare, + emit: emit, + listeners: listeners, + off: off, + on: on, + reset: reset, + run: run, + toString: toStringBench, + }) + + /* ------------------------------------------------------------------------*/ + + _.assign(Deferred.prototype, { + /** + * The deferred benchmark instance. + * + * @memberOf Benchmark.Deferred + * @type Object + */ + benchmark: null, - /* ------------------------------------------------------------------------*/ + /** + * The number of deferred cycles performed while benchmarking. + * + * @memberOf Benchmark.Deferred + * @type number + */ + cycles: 0, - /** - * The default options copied by suite instances. + /** + * The time taken to complete the deferred benchmark (secs). * - * @static - * @memberOf Benchmark.Suite - * @type Object + * @memberOf Benchmark.Deferred + * @type number */ - Suite.options = { + elapsed: 0, - /** - * The name of the suite. - * - * @memberOf Benchmark.Suite.options - * @type string - */ - 'name': undefined - }; + /** + * A timestamp of when the deferred benchmark started (ms). + * + * @memberOf Benchmark.Deferred + * @type number + */ + timeStamp: 0, + }) - /* ------------------------------------------------------------------------*/ + _.assign(Deferred.prototype, { resolve: resolve }) - _.assign(Suite.prototype, { + /* ------------------------------------------------------------------------*/ - /** - * The number of benchmarks in the suite. - * - * @memberOf Benchmark.Suite - * @type number - */ - 'length': 0, + _.assign(Event.prototype, { + /** + * A flag to indicate if the emitters listener iteration is aborted. + * + * @memberOf Benchmark.Event + * @type boolean + */ + aborted: false, - /** - * A flag to indicate if the suite is aborted. - * - * @memberOf Benchmark.Suite - * @type boolean - */ - 'aborted': false, + /** + * A flag to indicate if the default action is cancelled. + * + * @memberOf Benchmark.Event + * @type boolean + */ + cancelled: false, - /** - * A flag to indicate if the suite is running. - * - * @memberOf Benchmark.Suite - * @type boolean - */ - 'running': false - }); - - _.assign(Suite.prototype, { - 'abort': abortSuite, - 'add': add, - 'clone': cloneSuite, - 'emit': emit, - 'filter': filterSuite, - 'join': arrayRef.join, - 'listeners': listeners, - 'off': off, - 'on': on, - 'pop': arrayRef.pop, - 'push': push, - 'reset': resetSuite, - 'run': runSuite, - 'reverse': arrayRef.reverse, - 'shift': shift, - 'slice': slice, - 'sort': arrayRef.sort, - 'splice': arrayRef.splice, - 'unshift': unshift - }); - - /* ------------------------------------------------------------------------*/ - - // Expose Deferred, Event, and Suite. - _.assign(Benchmark, { - 'Deferred': Deferred, - 'Event': Event, - 'Suite': Suite - }); - - /* ------------------------------------------------------------------------*/ - - // Add lodash methods as Suite methods. - _.each(['each', 'forEach', 'indexOf', 'map', 'reduce'], (methodName) => { - const func = _[methodName]; - - Suite.prototype[methodName] = function() { - const args = [this]; - - push.apply(args, arguments); - - return func.apply(_, args); - }; - }); - - // Avoid array-like object bugs with `Array#shift` and `Array#splice` - // in Firefox < 10 and IE < 9. - _.each(['pop', 'shift', 'splice'], (methodName) => { - const func = arrayRef[methodName]; - - Suite.prototype[methodName] = function() { - let value = this, - result = func.apply(value, arguments); - - if (value.length === 0) { - delete value[0]; - } + /** + * The object whose listeners are currently being processed. + * + * @memberOf Benchmark.Event + * @type Object + */ + currentTarget: undefined, - return result; - }; - }); + /** + * The return value of the last executed listener. + * + * @memberOf Benchmark.Event + * @type Mixed + */ + result: undefined, + + /** + * The object to which the event was originally emitted. + * + * @memberOf Benchmark.Event + * @type Object + */ + target: undefined, - // Avoid buggy `Array#unshift` in IE < 8 which doesn't return the new - // length of the array. - Suite.prototype.unshift = function() { - const value = this; + /** + * A timestamp of when the event was created (ms). + * + * @memberOf Benchmark.Event + * @type number + */ + timeStamp: 0, - unshift.apply(value, arguments); + /** + * The event type. + * + * @memberOf Benchmark.Event + * @type string + */ + type: '', + }) - return value.length; - }; + /* ------------------------------------------------------------------------*/ - return Benchmark; + /** + * The default options copied by suite instances. + * + * @static + * @memberOf Benchmark.Suite + * @type Object + */ + Suite.options = { + /** + * The name of the suite. + * + * @memberOf Benchmark.Suite.options + * @type string + */ + name: undefined, } - /* --------------------------------------------------------------------------*/ + /* ------------------------------------------------------------------------*/ - // Export Benchmark. - // Some AMD build optimizers, like r.js, check for condition patterns like the following: - if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) { - // Define as an anonymous module so, through path mapping, it can be aliased. - define(['lodash', 'platform'], (_, platform) => runInContext({ - '_': _, - 'platform': platform - })); - } else { - const Benchmark = runInContext(); + _.assign(Suite.prototype, { + /** + * The number of benchmarks in the suite. + * + * @memberOf Benchmark.Suite + * @type number + */ + length: 0, - // Check for `exports` after `define` in case a build optimizer adds an `exports` object. - if (freeExports && freeModule) { - // Export for Node.js. - if (moduleExports) { - (freeModule.exports = Benchmark).Benchmark = Benchmark; - } - // Export for CommonJS support. - freeExports.Benchmark = Benchmark; - } else { - // Export to the global object. - root.Benchmark = Benchmark; + /** + * A flag to indicate if the suite is aborted. + * + * @memberOf Benchmark.Suite + * @type boolean + */ + aborted: false, + + /** + * A flag to indicate if the suite is running. + * + * @memberOf Benchmark.Suite + * @type boolean + */ + running: false, + }) + + _.assign(Suite.prototype, { + abort: abortSuite, + add: add, + clone: cloneSuite, + emit: emit, + filter: filterSuite, + join: arrayRef.join, + listeners: listeners, + off: off, + on: on, + pop: arrayRef.pop, + push: push, + reset: resetSuite, + run: runSuite, + reverse: arrayRef.reverse, + shift: shift, + slice: slice, + sort: arrayRef.sort, + splice: arrayRef.splice, + unshift: unshift, + }) + + /* ------------------------------------------------------------------------*/ + + // Expose Deferred, Event, and Suite. + _.assign(Benchmark, { + Deferred: Deferred, + Event: Event, + Suite: Suite, + }) + + /* ------------------------------------------------------------------------*/ + + // Add lodash methods as Suite methods. + _.each(['each', 'forEach', 'indexOf', 'map', 'reduce'], (methodName) => { + const func = _[methodName] + + Suite.prototype[methodName] = function () { + const args = [this] + + push.apply(args, arguments) + + return func.apply(_, args) + } + }) + + // Avoid array-like object bugs with `Array#shift` and `Array#splice` + // in Firefox < 10 and IE < 9. + _.each(['pop', 'shift', 'splice'], (methodName) => { + const func = arrayRef[methodName] + + Suite.prototype[methodName] = function () { + let value = this, + result = func.apply(value, arguments) + + if (value.length === 0) { + delete value[0] } + + return result + } + }) + + // Avoid buggy `Array#unshift` in IE < 8 which doesn't return the new + // length of the array. + Suite.prototype.unshift = function () { + const value = this + + unshift.apply(value, arguments) + + return value.length + } + + return Benchmark + } + + /* --------------------------------------------------------------------------*/ + + // Export Benchmark. + // Some AMD build optimizers, like r.js, check for condition patterns like the following: + if ( + typeof define == 'function' && + typeof define.amd == 'object' && + define.amd + ) { + // Define as an anonymous module so, through path mapping, it can be aliased. + define(['lodash', 'platform'], (_, platform) => + runInContext({ + _: _, + platform: platform, + })) + } else { + const Benchmark = runInContext() + + // Check for `exports` after `define` in case a build optimizer adds an `exports` object. + if (freeExports && freeModule) { + // Export for Node.js. + if (moduleExports) { + ;(freeModule.exports = Benchmark).Benchmark = Benchmark + } + // Export for CommonJS support. + freeExports.Benchmark = Benchmark + } else { + // Export to the global object. + root.Benchmark = Benchmark } -}.call(this)); + } +}.call(this)) diff --git a/src/vendor/source-map-support.js b/src/vendor/source-map-support.js index 1f0769b9..c8cd54c3 100644 --- a/src/vendor/source-map-support.js +++ b/src/vendor/source-map-support.js @@ -1,21 +1,22 @@ -'use strict'; +// @ts-nocheck +'use strict' -const SourceMapConsumer = require('source-map').SourceMapConsumer; -const path = require('path'); +const SourceMapConsumer = require('source-map').SourceMapConsumer +const path = require('path') -let fs; +let fs try { - fs = require('fs'); - if (!fs.existsSync || !fs.readFileSync) { + fs = require('fs') + if (!fs.existsSync || !fs.readFileSync) { // fs doesn't have all methods we need - fs = null; - } + fs = null + } } catch (err) { - /* nop */ + /* nop */ } -const { Buffer } = require('buffer'); +const { Buffer } = require('buffer') /** * Requires a module which is protected against bundler minification. @@ -24,162 +25,180 @@ const { Buffer } = require('buffer'); * @param {string} request */ function dynamicRequire(mod, request) { - return mod.require(request); + return mod.require(request) } // Only install once if called multiple times -let errorFormatterInstalled = false; -let uncaughtShimInstalled = false; +let errorFormatterInstalled = false +let uncaughtShimInstalled = false // If true, the caches are reset before a stack trace formatting operation -let emptyCacheBetweenOperations = false; +let emptyCacheBetweenOperations = false // Supports {browser, node, auto} -let environment = 'auto'; +let environment = 'auto' // Maps a file path to a string containing the file contents -let fileContentsCache = {}; +let fileContentsCache = {} // Maps a file path to a source map for that file -let sourceMapCache = {}; +let sourceMapCache = {} // Regex for detecting source maps -const reSourceMap = /^data:application\/json[^,]+base64,/; +const reSourceMap = /^data:application\/json[^,]+base64,/ // Priority list of retrieve handlers -let retrieveFileHandlers = []; -let retrieveMapHandlers = []; +let retrieveFileHandlers = [] +let retrieveMapHandlers = [] function isInBrowser() { - if (environment === 'browser') { - return true; - } - if (environment === 'node') { - return false; - } - - return ((typeof window !== 'undefined') && (typeof XMLHttpRequest === 'function') && !(window.require && window.module && window.process && window.process.type === 'renderer')); + if (environment === 'browser') { + return true + } + if (environment === 'node') { + return false + } + + return ( + typeof window !== 'undefined' && + typeof XMLHttpRequest === 'function' && + !( + window.require && + window.module && + window.process && + window.process.type === 'renderer' + ) + ) } function hasGlobalProcessEventEmitter() { - return ((typeof process === 'object') && (process !== null) && (typeof process.on === 'function')); + return ( + typeof process === 'object' && + process !== null && + typeof process.on === 'function' + ) } function handlerExec(list) { - return function(arg) { - for (let i = 0; i < list.length; i++) { - const ret = list[i](arg); + return function (arg) { + for (let i = 0; i < list.length; i++) { + const ret = list[i](arg) - if (ret) { - return ret; - } - } + if (ret) { + return ret + } + } - return null; - }; + return null + } } -let retrieveFile = handlerExec(retrieveFileHandlers); +let retrieveFile = handlerExec(retrieveFileHandlers) retrieveFileHandlers.push((path) => { - // Trim the path to make sure there is no extra whitespace. - path = path.trim(); - if (/^file:/.test(path)) { + // Trim the path to make sure there is no extra whitespace. + path = path.trim() + if (/^file:/.test(path)) { // existsSync/readFileSync can't handle file protocol, but once stripped, it works - path = path.replace(/file:\/\/\/(\w:)?/, (protocol, drive) => - (drive ? - '' : // file:///C:/dir/file -> C:/dir/file - '/') // file:///root-dir/file -> /root-dir/file - ); - } - if (path in fileContentsCache) { - return fileContentsCache[path]; - } - - let contents = ''; - - try { - if (!fs) { - // Use SJAX if we are in the browser - const xhr = new XMLHttpRequest(); - - xhr.open('GET', path, /** async */ false); - xhr.send(null); - if (xhr.readyState === 4 && xhr.status === 200) { - contents = xhr.responseText; - } - } else if (fs.existsSync(path)) { - // Otherwise, use the filesystem - contents = fs.readFileSync(path, 'utf8'); - } - } catch (er) { + path = path.replace( + /file:\/\/\/(\w:)?/, + (protocol, drive) => + drive + ? '' // file:///C:/dir/file -> C:/dir/file + : '/' // file:///root-dir/file -> /root-dir/file + ) + } + if (path in fileContentsCache) { + return fileContentsCache[path] + } + + let contents = '' + + try { + if (!fs) { + // Use SJAX if we are in the browser + const xhr = new XMLHttpRequest() + + xhr.open('GET', path, /** async */ false) + xhr.send(null) + if (xhr.readyState === 4 && xhr.status === 200) { + contents = xhr.responseText + } + } else if (fs.existsSync(path)) { + // Otherwise, use the filesystem + contents = fs.readFileSync(path, 'utf8') + } + } catch (er) { /* ignore any errors */ - } + } - return fileContentsCache[path] = contents; -}); + return (fileContentsCache[path] = contents) +}) // Support URLs relative to a directory, but be careful about a protocol prefix // in case we are in the browser (i.e. directories may start with "http://" or "file:///") function supportRelativeURL(file, url, tweak) { - if (!file) { - return url; - } - const dir = path.dirname(file); - const match = /^\w+:\/\/[^\/]*/.exec(dir); - let protocol = match ? match[0] : ''; - const startPath = dir.slice(protocol.length); - - if (protocol && /^\/\w\:/.test(startPath)) { + if (!file) { + return url + } + const dir = path.dirname(file) + const match = /^\w+:\/\/[^\/]*/.exec(dir) + let protocol = match ? match[0] : '' + const startPath = dir.slice(protocol.length) + + if (protocol && /^\/\w\:/.test(startPath)) { // handle file:///C:/ paths - protocol += '/'; - - return protocol + path.resolve(dir.slice(protocol.length), url).replace(/\\/g, '/'); - } - if (tweak && PW_TEST_SOURCEMAP === true) { - return 'file://' + path.resolve(dir.slice(protocol.length), url); - } - - return protocol + path.resolve(dir.slice(protocol.length), url); + protocol += '/' + + return ( + protocol + + path.resolve(dir.slice(protocol.length), url).replace(/\\/g, '/') + ) + } + if (tweak && PW_TEST_SOURCEMAP === true) { + return 'file://' + path.resolve(dir.slice(protocol.length), url) + } + + return protocol + path.resolve(dir.slice(protocol.length), url) } function retrieveSourceMapURL(source) { - let fileData; - - if (isInBrowser()) { - try { - const xhr = new XMLHttpRequest(); - - xhr.open('GET', source, false); - xhr.send(null); - fileData = xhr.readyState === 4 ? xhr.responseText : null; - - // Support providing a sourceMappingURL via the SourceMap header - const sourceMapHeader = xhr.getResponseHeader('SourceMap') || - xhr.getResponseHeader('X-SourceMap'); - - if (sourceMapHeader) { - return sourceMapHeader; - } - } catch (e) { - } - } - - // Get the URL of the source map - fileData = retrieveFile(source); - const re = /(?:\/\/[@#][\s]*sourceMappingURL=([^\s'"]+)[\s]*$)|(?:\/\*[@#][\s]*sourceMappingURL=([^\s*'"]+)[\s]*(?:\*\/)[\s]*$)/mg; - // Keep executing the search to find the *last* sourceMappingURL to avoid - // picking up sourceMappingURLs from comments, strings, etc. - let lastMatch, match; - - while (match = re.exec(fileData)) { - lastMatch = match; - } - if (!lastMatch) { - return null; - } + let fileData - return lastMatch[1]; + if (isInBrowser()) { + try { + const xhr = new XMLHttpRequest() + + xhr.open('GET', source, false) + xhr.send(null) + fileData = xhr.readyState === 4 ? xhr.responseText : null + + // Support providing a sourceMappingURL via the SourceMap header + const sourceMapHeader = + xhr.getResponseHeader('SourceMap') || + xhr.getResponseHeader('X-SourceMap') + + if (sourceMapHeader) { + return sourceMapHeader + } + } catch (e) {} + } + + // Get the URL of the source map + fileData = retrieveFile(source) + const re = /(?:\/\/[@#][\s]*sourceMappingURL=([^\s'"]+)[\s]*$)|(?:\/\*[@#][\s]*sourceMappingURL=([^\s*'"]+)[\s]*(?:\*\/)[\s]*$)/gm + // Keep executing the search to find the *last* sourceMappingURL to avoid + // picking up sourceMappingURLs from comments, strings, etc. + let lastMatch, match + + while ((match = re.exec(fileData))) { + lastMatch = match + } + if (!lastMatch) { + return null + } + + return lastMatch[1] } // Can be overridden by the retrieveSourceMap option to install. Takes a @@ -187,119 +206,135 @@ function retrieveSourceMapURL(source) { // there is no source map. The map field may be either a string or the parsed // JSON object (ie, it must be a valid argument to the SourceMapConsumer // constructor). -let retrieveSourceMap = handlerExec(retrieveMapHandlers); +let retrieveSourceMap = handlerExec(retrieveMapHandlers) retrieveMapHandlers.push((source) => { - let sourceMappingURL = retrieveSourceMapURL(source); + let sourceMappingURL = retrieveSourceMapURL(source) - if (!sourceMappingURL) { - return null; - } + if (!sourceMappingURL) { + return null + } - // Read the contents of the source map - let sourceMapData; + // Read the contents of the source map + let sourceMapData - if (reSourceMap.test(sourceMappingURL)) { + if (reSourceMap.test(sourceMappingURL)) { // Support source map URL as a data url - const rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(',') + 1); + const rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(',') + 1) - sourceMapData = Buffer.from(rawData, 'base64').toString(); - sourceMappingURL = source; - } else { + sourceMapData = Buffer.from(rawData, 'base64').toString() + sourceMappingURL = source + } else { // Support source map URLs relative to the source URL - sourceMappingURL = supportRelativeURL(source, sourceMappingURL); - sourceMapData = retrieveFile(sourceMappingURL); - } + sourceMappingURL = supportRelativeURL(source, sourceMappingURL) + sourceMapData = retrieveFile(sourceMappingURL) + } - if (!sourceMapData) { - return null; - } + if (!sourceMapData) { + return null + } - return { - url: sourceMappingURL, - map: sourceMapData - }; -}); + return { + url: sourceMappingURL, + map: sourceMapData, + } +}) function mapSourcePosition(position) { - let sourceMap = sourceMapCache[position.source]; + let sourceMap = sourceMapCache[position.source] - if (!sourceMap) { + if (!sourceMap) { // Call the (overrideable) retrieveSourceMap function to get the source map. - const urlAndMap = retrieveSourceMap(position.source); - - if (urlAndMap) { - sourceMap = sourceMapCache[position.source] = { - url: urlAndMap.url, - map: new SourceMapConsumer(urlAndMap.map) - }; - - // Load all sources stored inline with the source map into the file cache - // to pretend like they are already loaded. They may not exist on disk. - if (sourceMap.map.sourcesContent) { - sourceMap.map.sources.forEach((source, i) => { - const contents = sourceMap.map.sourcesContent[i]; - - if (contents) { - const url = supportRelativeURL(sourceMap.url, source, true); - - fileContentsCache[url] = contents; - } - }); - } - } else { - sourceMap = sourceMapCache[position.source] = { - url: null, - map: null - }; - } - } - - // Resolve the source URL relative to the URL of the source map - if (sourceMap && sourceMap.map && typeof sourceMap.map.originalPositionFor === 'function') { - const originalPosition = sourceMap.map.originalPositionFor(position); - - // Only return the original position if a matching line was found. If no - // matching line is found then we return position instead, which will cause - // the stack trace to print the path and line for the compiled file. It is - // better to give a precise location in the compiled file than a vague - // location in the original file. - if (originalPosition.source !== null) { - originalPosition.source = supportRelativeURL( - sourceMap.url, originalPosition.source, true); - - return originalPosition; - } - } - - return position; + const urlAndMap = retrieveSourceMap(position.source) + + if (urlAndMap) { + sourceMap = sourceMapCache[position.source] = { + url: urlAndMap.url, + map: new SourceMapConsumer(urlAndMap.map), + } + + // Load all sources stored inline with the source map into the file cache + // to pretend like they are already loaded. They may not exist on disk. + if (sourceMap.map.sourcesContent) { + sourceMap.map.sources.forEach((source, i) => { + const contents = sourceMap.map.sourcesContent[i] + + if (contents) { + const url = supportRelativeURL(sourceMap.url, source, true) + + fileContentsCache[url] = contents + } + }) + } + } else { + sourceMap = sourceMapCache[position.source] = { + url: null, + map: null, + } + } + } + + // Resolve the source URL relative to the URL of the source map + if ( + sourceMap && + sourceMap.map && + typeof sourceMap.map.originalPositionFor === 'function' + ) { + const originalPosition = sourceMap.map.originalPositionFor(position) + + // Only return the original position if a matching line was found. If no + // matching line is found then we return position instead, which will cause + // the stack trace to print the path and line for the compiled file. It is + // better to give a precise location in the compiled file than a vague + // location in the original file. + if (originalPosition.source !== null) { + originalPosition.source = supportRelativeURL( + sourceMap.url, + originalPosition.source, + true + ) + + return originalPosition + } + } + + return position } // Parses code generated by FormatEvalOrigin(), a function inside V8: // https://code.google.com/p/v8/source/browse/trunk/src/messages.js function mapEvalOrigin(origin) { - // Most eval() calls are in this format - let match = /^eval at ([^(]+) \((.+):(\d+):(\d+)\)$/.exec(origin); - - if (match) { - const position = mapSourcePosition({ - source: match[2], - line: Number(match[3]), - column: match[4] - 1 - }); - - return 'eval at ' + match[1] + ' (' + position.source + ':' + - position.line + ':' + (position.column + 1) + ')'; - } - - // Parse nested eval() calls using recursion - match = /^eval at ([^(]+) \((.+)\)$/.exec(origin); - if (match) { - return 'eval at ' + match[1] + ' (' + mapEvalOrigin(match[2]) + ')'; - } - - // Make sure we still return useful information if we didn't find anything - return origin; + // Most eval() calls are in this format + let match = /^eval at ([^(]+) \((.+):(\d+):(\d+)\)$/.exec(origin) + + if (match) { + const position = mapSourcePosition({ + source: match[2], + line: Number(match[3]), + column: match[4] - 1, + }) + + return ( + 'eval at ' + + match[1] + + ' (' + + position.source + + ':' + + position.line + + ':' + + (position.column + 1) + + ')' + ) + } + + // Parse nested eval() calls using recursion + match = /^eval at ([^(]+) \((.+)\)$/.exec(origin) + if (match) { + return 'eval at ' + match[1] + ' (' + mapEvalOrigin(match[2]) + ')' + } + + // Make sure we still return useful information if we didn't find anything + return origin } // This is copied almost verbatim from the V8 source code at @@ -309,378 +344,405 @@ function mapEvalOrigin(origin) { // did something to the prototype chain and broke the shim. The only fix I // could find was copy/paste. function CallSiteToString() { - let fileName; - let fileLocation = ''; - - if (this.isNative()) { - fileLocation = 'native'; - } else { - fileName = this.getScriptNameOrSourceURL(); - if (!fileName && this.isEval()) { - fileLocation = this.getEvalOrigin(); - fileLocation += ', '; // Expecting source position to follow. - } - - if (fileName) { - fileLocation += fileName; - } else { - // Source code does not originate from a file and is not native, but we - // can still get the source position inside the source string, e.g. in - // an eval string. - fileLocation += ''; - } - const lineNumber = this.getLineNumber(); - - if (lineNumber != null) { - fileLocation += ':' + lineNumber; - const columnNumber = this.getColumnNumber(); + let fileName + let fileLocation = '' - if (columnNumber) { - fileLocation += ':' + columnNumber; - } - } + if (this.isNative()) { + fileLocation = 'native' + } else { + fileName = this.getScriptNameOrSourceURL() + if (!fileName && this.isEval()) { + fileLocation = this.getEvalOrigin() + fileLocation += ', ' // Expecting source position to follow. } - let line = ''; - const functionName = this.getFunctionName(); - let addSuffix = true; - const isConstructor = this.isConstructor(); - const isMethodCall = !(this.isToplevel() || isConstructor); - - if (isMethodCall) { - let typeName = this.getTypeName(); - - // Fixes shim to be backward compatable with Node v0 to v4 - if (typeName === '[object Object]') { - typeName = 'null'; - } - const methodName = this.getMethodName(); - - if (functionName) { - if (typeName && functionName.indexOf(typeName) != 0) { - line += typeName + '.'; - } - line += functionName; - if (methodName && functionName.indexOf('.' + methodName) != functionName.length - methodName.length - 1) { - line += ' [as ' + methodName + ']'; - } - } else { - line += typeName + '.' + (methodName || ''); - } - } else if (isConstructor) { - line += 'new ' + (functionName || ''); - } else if (functionName) { - line += functionName; + if (fileName) { + fileLocation += fileName } else { - line += fileLocation; - addSuffix = false; - } - if (addSuffix) { - line += ' (' + fileLocation + ')'; - } - - return line; + // Source code does not originate from a file and is not native, but we + // can still get the source position inside the source string, e.g. in + // an eval string. + fileLocation += '' + } + const lineNumber = this.getLineNumber() + + if (lineNumber != null) { + fileLocation += ':' + lineNumber + const columnNumber = this.getColumnNumber() + + if (columnNumber) { + fileLocation += ':' + columnNumber + } + } + } + + let line = '' + const functionName = this.getFunctionName() + let addSuffix = true + const isConstructor = this.isConstructor() + const isMethodCall = !(this.isToplevel() || isConstructor) + + if (isMethodCall) { + let typeName = this.getTypeName() + + // Fixes shim to be backward compatable with Node v0 to v4 + if (typeName === '[object Object]') { + typeName = 'null' + } + const methodName = this.getMethodName() + + if (functionName) { + if (typeName && functionName.indexOf(typeName) != 0) { + line += typeName + '.' + } + line += functionName + if ( + methodName && + functionName.indexOf('.' + methodName) != + functionName.length - methodName.length - 1 + ) { + line += ' [as ' + methodName + ']' + } + } else { + line += typeName + '.' + (methodName || '') + } + } else if (isConstructor) { + line += 'new ' + (functionName || '') + } else if (functionName) { + line += functionName + } else { + line += fileLocation + addSuffix = false + } + if (addSuffix) { + line += ' (' + fileLocation + ')' + } + + return line } function cloneCallSite(frame) { - const object = {}; + const object = {} - Object.getOwnPropertyNames(Object.getPrototypeOf(frame)).forEach((name) => { - object[name] = /^(?:is|get)/.test(name) ? function() { - return frame[name].call(frame); - } : frame[name]; - }); - object.toString = CallSiteToString; + Object.getOwnPropertyNames(Object.getPrototypeOf(frame)).forEach((name) => { + object[name] = /^(?:is|get)/.test(name) + ? function () { + return frame[name].call(frame) + } + : frame[name] + }) + object.toString = CallSiteToString - return object; + return object } function wrapCallSite(frame, state) { - // provides interface backward compatibility - if (state === undefined) { - state = { - nextPosition: null, - curPosition: null - }; + // provides interface backward compatibility + if (state === undefined) { + state = { + nextPosition: null, + curPosition: null, } - if (frame.isNative()) { - state.curPosition = null; + } + if (frame.isNative()) { + state.curPosition = null - return frame; + return frame + } + + // Most call sites will return the source file from getFileName(), but code + // passed to eval() ending in "//# sourceURL=..." will return the source file + // from getScriptNameOrSourceURL() instead + const source = frame.getFileName() || frame.getScriptNameOrSourceURL() + + if (source) { + const line = frame.getLineNumber() + let column = frame.getColumnNumber() - 1 + + // Fix position in Node where some (internal) code is prepended. + // See https://github.com/evanw/node-source-map-support/issues/36 + // Header removed in node at ^10.16 || >=11.11.0 + // v11 is not an LTS candidate, we can just test the one version with it. + // Test node versions for: 10.16-19, 10.20+, 12-19, 20-99, 100+, or 11.11 + const noHeader = /^v(10\.1[6-9]|10\.[2-9][0-9]|10\.[0-9]{3,}|1[2-9]\d*|[2-9]\d|\d{3,}|11\.11)/ + const headerLength = noHeader.test(process.version) ? 0 : 62 + + if ( + line === 1 && + column > headerLength && + !isInBrowser() && + !frame.isEval() + ) { + column -= headerLength } - // Most call sites will return the source file from getFileName(), but code - // passed to eval() ending in "//# sourceURL=..." will return the source file - // from getScriptNameOrSourceURL() instead - const source = frame.getFileName() || frame.getScriptNameOrSourceURL(); - - if (source) { - const line = frame.getLineNumber(); - let column = frame.getColumnNumber() - 1; - - // Fix position in Node where some (internal) code is prepended. - // See https://github.com/evanw/node-source-map-support/issues/36 - // Header removed in node at ^10.16 || >=11.11.0 - // v11 is not an LTS candidate, we can just test the one version with it. - // Test node versions for: 10.16-19, 10.20+, 12-19, 20-99, 100+, or 11.11 - const noHeader = /^v(10\.1[6-9]|10\.[2-9][0-9]|10\.[0-9]{3,}|1[2-9]\d*|[2-9]\d|\d{3,}|11\.11)/; - const headerLength = noHeader.test(process.version) ? 0 : 62; - - if (line === 1 && column > headerLength && !isInBrowser() && !frame.isEval()) { - column -= headerLength; - } + const position = mapSourcePosition({ + source: source, + line: line, + column: column, + }) + + state.curPosition = position + frame = cloneCallSite(frame) + const originalFunctionName = frame.getFunctionName + + frame.getFunctionName = function () { + if (state.nextPosition == null) { + return originalFunctionName() + } - const position = mapSourcePosition({ - source: source, - line: line, - column: column - }); - - state.curPosition = position; - frame = cloneCallSite(frame); - const originalFunctionName = frame.getFunctionName; - - frame.getFunctionName = function() { - if (state.nextPosition == null) { - return originalFunctionName(); - } - - return state.nextPosition.name || originalFunctionName(); - }; - frame.getFileName = function() { - return position.source; - }; - frame.getLineNumber = function() { - return position.line; - }; - frame.getColumnNumber = function() { - return position.column + 1; - }; - frame.getScriptNameOrSourceURL = function() { - return position.source; - }; - - return frame; + return state.nextPosition.name || originalFunctionName() + } + frame.getFileName = function () { + return position.source + } + frame.getLineNumber = function () { + return position.line + } + frame.getColumnNumber = function () { + return position.column + 1 + } + frame.getScriptNameOrSourceURL = function () { + return position.source } - // Code called using eval() needs special handling - let origin = frame.isEval() && frame.getEvalOrigin(); + return frame + } - if (origin) { - origin = mapEvalOrigin(origin); - frame = cloneCallSite(frame); - frame.getEvalOrigin = function() { - return origin; - }; + // Code called using eval() needs special handling + let origin = frame.isEval() && frame.getEvalOrigin() - return frame; + if (origin) { + origin = mapEvalOrigin(origin) + frame = cloneCallSite(frame) + frame.getEvalOrigin = function () { + return origin } - // If we get here then we were unable to change the source position - return frame; + return frame + } + + // If we get here then we were unable to change the source position + return frame } // This function is part of the V8 stack trace API, for more info see: // https://v8.dev/docs/stack-trace-api function prepareStackTrace(error, stack) { - if (emptyCacheBetweenOperations) { - fileContentsCache = {}; - sourceMapCache = {}; - } - - const name = error.name || 'Error'; - const message = error.message || ''; - const errorString = name + ': ' + message; - - const state = { - nextPosition: null, - curPosition: null - }; - const processedStack = []; - - for (let i = stack.length - 1; i >= 0; i--) { - processedStack.push('\n at ' + wrapCallSite(stack[i], state)); - state.nextPosition = state.curPosition; - } - state.curPosition = state.nextPosition = null; - - return errorString + processedStack.reverse().join(''); + if (emptyCacheBetweenOperations) { + fileContentsCache = {} + sourceMapCache = {} + } + + const name = error.name || 'Error' + const message = error.message || '' + const errorString = name + ': ' + message + + const state = { + nextPosition: null, + curPosition: null, + } + const processedStack = [] + + for (let i = stack.length - 1; i >= 0; i--) { + processedStack.push('\n at ' + wrapCallSite(stack[i], state)) + state.nextPosition = state.curPosition + } + state.curPosition = state.nextPosition = null + + return errorString + processedStack.reverse().join('') } // Generate position and snippet of original source with pointer function getErrorSource(error) { - const match = /\n {4}at [^(]+ \((.*):(\d+):(\d+)\)/.exec(error.stack); - - if (match) { - const source = match[1]; - const line = Number(match[2]); - const column = Number(match[3]); - - // Support the inline sourceContents inside the source map - let contents = fileContentsCache[source]; - - // Support files on disk - if (!contents && fs && fs.existsSync(source)) { - try { - contents = fs.readFileSync(source, 'utf8'); - } catch (er) { - contents = ''; - } - } - - // Format the line from the original source code like node does - if (contents) { - const code = contents.split(/(?:\r\n|\r|\n)/)[line - 1]; - - if (code) { - return source + ':' + line + '\n' + code + '\n' + - new Array(column).join(' ') + '^'; - } - } - } - - return null; + const match = /\n {4}at [^(]+ \((.*):(\d+):(\d+)\)/.exec(error.stack) + + if (match) { + const source = match[1] + const line = Number(match[2]) + const column = Number(match[3]) + + // Support the inline sourceContents inside the source map + let contents = fileContentsCache[source] + + // Support files on disk + if (!contents && fs && fs.existsSync(source)) { + try { + contents = fs.readFileSync(source, 'utf8') + } catch (er) { + contents = '' + } + } + + // Format the line from the original source code like node does + if (contents) { + const code = contents.split(/(?:\r\n|\r|\n)/)[line - 1] + + if (code) { + return ( + source + + ':' + + line + + '\n' + + code + + '\n' + + new Array(column).join(' ') + + '^' + ) + } + } + } + + return null } function printErrorAndExit(error) { - const source = getErrorSource(error); + const source = getErrorSource(error) - // Ensure error is printed synchronously and not truncated - if (process.stderr._handle && process.stderr._handle.setBlocking) { - process.stderr._handle.setBlocking(true); - } + // Ensure error is printed synchronously and not truncated + if (process.stderr._handle && process.stderr._handle.setBlocking) { + process.stderr._handle.setBlocking(true) + } - if (source) { - console.error(); - console.error(source); - } + if (source) { + console.error() + console.error(source) + } - console.error(error.stack); - process.exit(1); + console.error(error.stack) + process.exit(1) } function shimEmitUncaughtException() { - const origEmit = process.emit; + const origEmit = process.emit - process.emit = function(type) { - if (type === 'uncaughtException') { - const hasStack = (arguments[1] && arguments[1].stack); - const hasListeners = (this.listeners(type).length > 0); + process.emit = function (type) { + if (type === 'uncaughtException') { + const hasStack = arguments[1] && arguments[1].stack + const hasListeners = this.listeners(type).length > 0 - if (hasStack && !hasListeners) { - return printErrorAndExit(arguments[1]); - } - } + if (hasStack && !hasListeners) { + return printErrorAndExit(arguments[1]) + } + } - return origEmit.apply(this, arguments); - }; + return origEmit.apply(this, arguments) + } } -const originalRetrieveFileHandlers = retrieveFileHandlers.slice(0); -const originalRetrieveMapHandlers = retrieveMapHandlers.slice(0); +const originalRetrieveFileHandlers = retrieveFileHandlers.slice(0) +const originalRetrieveMapHandlers = retrieveMapHandlers.slice(0) -exports.wrapCallSite = wrapCallSite; -exports.getErrorSource = getErrorSource; -exports.mapSourcePosition = mapSourcePosition; -exports.retrieveSourceMap = retrieveSourceMap; +exports.wrapCallSite = wrapCallSite +exports.getErrorSource = getErrorSource +exports.mapSourcePosition = mapSourcePosition +exports.retrieveSourceMap = retrieveSourceMap -exports.install = function(options) { - options = options || {}; +exports.install = function (options) { + options = options || {} - if (options.environment) { - environment = options.environment; - if (['node', 'browser', 'auto'].indexOf(environment) === -1) { - throw new Error('environment ' + environment + ' was unknown. Available options are {auto, browser, node}'); - } + if (options.environment) { + environment = options.environment + if (['node', 'browser', 'auto'].indexOf(environment) === -1) { + throw new Error( + 'environment ' + + environment + + ' was unknown. Available options are {auto, browser, node}' + ) } + } - // Allow sources to be found by methods other than reading the files - // directly from disk. - if (options.retrieveFile) { - if (options.overrideRetrieveFile) { - retrieveFileHandlers.length = 0; - } - - retrieveFileHandlers.unshift(options.retrieveFile); + // Allow sources to be found by methods other than reading the files + // directly from disk. + if (options.retrieveFile) { + if (options.overrideRetrieveFile) { + retrieveFileHandlers.length = 0 } - // Allow source maps to be found by methods other than reading the files - // directly from disk. - if (options.retrieveSourceMap) { - if (options.overrideRetrieveSourceMap) { - retrieveMapHandlers.length = 0; - } + retrieveFileHandlers.unshift(options.retrieveFile) + } - retrieveMapHandlers.unshift(options.retrieveSourceMap); + // Allow source maps to be found by methods other than reading the files + // directly from disk. + if (options.retrieveSourceMap) { + if (options.overrideRetrieveSourceMap) { + retrieveMapHandlers.length = 0 } - // Support runtime transpilers that include inline source maps - if (options.hookRequire && !isInBrowser()) { - // Use dynamicRequire to avoid including in browser bundles - const Module = dynamicRequire(module, 'module'); - const $compile = Module.prototype._compile; - - if (!$compile.__sourceMapSupport) { - Module.prototype._compile = function(content, filename) { - fileContentsCache[filename] = content; - sourceMapCache[filename] = undefined; - - return $compile.call(this, content, filename); - }; + retrieveMapHandlers.unshift(options.retrieveSourceMap) + } - Module.prototype._compile.__sourceMapSupport = true; - } - } - - // Configure options - if (!emptyCacheBetweenOperations) { - emptyCacheBetweenOperations = 'emptyCacheBetweenOperations' in options ? - options.emptyCacheBetweenOperations : false; - } - - // Install the error reformatter - if (!errorFormatterInstalled) { - errorFormatterInstalled = true; - Error.prepareStackTrace = prepareStackTrace; - } - - if (!uncaughtShimInstalled) { - let installHandler = 'handleUncaughtExceptions' in options ? - options.handleUncaughtExceptions : true; - - // Do not override 'uncaughtException' with our own handler in Node.js - // Worker threads. Workers pass the error to the main thread as an event, - // rather than printing something to stderr and exiting. - try { - // We need to use `dynamicRequire` because `require` on it's own will be optimized by WebPack/Browserify. - const worker_threads = dynamicRequire(module, 'worker_threads'); - - if (worker_threads.isMainThread === false) { - installHandler = false; - } - } catch (e) {} - - // Provide the option to not install the uncaught exception handler. This is - // to support other uncaught exception handlers (in test frameworks, for - // example). If this handler is not installed and there are no other uncaught - // exception handlers, uncaught exceptions will be caught by node's built-in - // exception handler and the process will still be terminated. However, the - // generated JavaScript code will be shown above the stack trace instead of - // the original source code. - if (installHandler && hasGlobalProcessEventEmitter()) { - uncaughtShimInstalled = true; - shimEmitUncaughtException(); - } - } -}; + // Support runtime transpilers that include inline source maps + if (options.hookRequire && !isInBrowser()) { + // Use dynamicRequire to avoid including in browser bundles + const Module = dynamicRequire(module, 'module') + const $compile = Module.prototype._compile + + if (!$compile.__sourceMapSupport) { + Module.prototype._compile = function (content, filename) { + fileContentsCache[filename] = content + sourceMapCache[filename] = undefined + + return $compile.call(this, content, filename) + } + + Module.prototype._compile.__sourceMapSupport = true + } + } + + // Configure options + if (!emptyCacheBetweenOperations) { + emptyCacheBetweenOperations = + 'emptyCacheBetweenOperations' in options + ? options.emptyCacheBetweenOperations + : false + } + + // Install the error reformatter + if (!errorFormatterInstalled) { + errorFormatterInstalled = true + Error.prepareStackTrace = prepareStackTrace + } + + if (!uncaughtShimInstalled) { + let installHandler = + 'handleUncaughtExceptions' in options + ? options.handleUncaughtExceptions + : true + + // Do not override 'uncaughtException' with our own handler in Node.js + // Worker threads. Workers pass the error to the main thread as an event, + // rather than printing something to stderr and exiting. + try { + // We need to use `dynamicRequire` because `require` on it's own will be optimized by WebPack/Browserify. + const worker_threads = dynamicRequire(module, 'worker_threads') + + if (worker_threads.isMainThread === false) { + installHandler = false + } + } catch (e) {} + + // Provide the option to not install the uncaught exception handler. This is + // to support other uncaught exception handlers (in test frameworks, for + // example). If this handler is not installed and there are no other uncaught + // exception handlers, uncaught exceptions will be caught by node's built-in + // exception handler and the process will still be terminated. However, the + // generated JavaScript code will be shown above the stack trace instead of + // the original source code. + if (installHandler && hasGlobalProcessEventEmitter()) { + uncaughtShimInstalled = true + shimEmitUncaughtException() + } + } +} -exports.resetRetrieveHandlers = function() { - retrieveFileHandlers.length = 0; - retrieveMapHandlers.length = 0; +exports.resetRetrieveHandlers = function () { + retrieveFileHandlers.length = 0 + retrieveMapHandlers.length = 0 - retrieveFileHandlers = originalRetrieveFileHandlers.slice(0); - retrieveMapHandlers = originalRetrieveMapHandlers.slice(0); + retrieveFileHandlers = originalRetrieveFileHandlers.slice(0) + retrieveMapHandlers = originalRetrieveMapHandlers.slice(0) - retrieveSourceMap = handlerExec(retrieveMapHandlers); - retrieveFile = handlerExec(retrieveFileHandlers); -}; + retrieveSourceMap = handlerExec(retrieveMapHandlers) + retrieveFile = handlerExec(retrieveFileHandlers) +} diff --git a/static/background.js b/static/background.js index 59b9952c..cec957e4 100644 --- a/static/background.js +++ b/static/background.js @@ -1,7 +1,10 @@ +/* eslint-disable no-console */ /* eslint-disable no-undef */ /* eslint-disable strict */ chrome.browserAction.onClicked.addListener((tab) => { - // No tabs or host permissions needed! - console.log('Turning ' + tab.url + ' red!'); - chrome.tabs.executeScript({ code: 'document.body.style.backgroundColor="red"' }); -}); + // No tabs or host permissions needed! + console.log('Turning ' + tab.url + ' red!') + chrome.tabs.executeScript({ + code: 'document.body.style.backgroundColor="red"', + }) +}) diff --git a/static/before.html b/static/before.html index 06489e98..5b385012 100644 --- a/static/before.html +++ b/static/before.html @@ -5,6 +5,5 @@ Extra - - + diff --git a/static/index.html b/static/index.html index aef7ad55..f00b002b 100644 --- a/static/index.html +++ b/static/index.html @@ -5,6 +5,5 @@ Mocha Tests - - + diff --git a/static/manifest.json b/static/manifest.json index c4ed4413..21fd1922 100644 --- a/static/manifest.json +++ b/static/manifest.json @@ -1,18 +1,15 @@ { - "name": "Stub Extension", - "description": "Stub extension to run tests.", - "version": "2.0", - "background": { - "scripts": ["background.js"], - "persistent": true - }, - "permissions" :[ - "developerPrivate", - "management" - ], - "browser_action": { - "default_title": "Make this page red" - }, - "manifest_version": 2, - "content_security_policy": "script-src 'self' 'unsafe-eval'" - } + "name": "Stub Extension", + "description": "Stub extension to run tests.", + "version": "2.0", + "background": { + "scripts": ["background.js"], + "persistent": true + }, + "permissions": ["developerPrivate", "management"], + "browser_action": { + "default_title": "Make this page red" + }, + "manifest_version": 2, + "content_security_policy": "script-src 'self' 'unsafe-eval'" +} diff --git a/static/setup.js b/static/setup.js index 7a1de037..23aa47b4 100644 --- a/static/setup.js +++ b/static/setup.js @@ -1,25 +1,25 @@ /* eslint-disable strict */ class PwTestController { - constructor() { - this.beforeEnded = false; - this.ended = false; - this.failed = false; - this.env = {}; - } + constructor() { + this.beforeEnded = false + this.ended = false + this.failed = false + this.env = {} + } - beforeEnd() { - this.beforeEnded = true; - } + beforeEnd() { + this.beforeEnded = true + } - end(failed = this.failed) { - this.ended = true; - this.failed = failed; - } + end(failed = this.failed) { + this.ended = true + this.failed = failed + } - fail() { - this.failed = true; - } + fail() { + this.failed = true + } } -self.PW_TEST = new PwTestController(); +self.PW_TEST = new PwTestController() diff --git a/test.js b/test.js index 40a1f0ab..c70fa301 100644 --- a/test.js +++ b/test.js @@ -1,87 +1,115 @@ -'use strict'; +'use strict' -const { test } = require('uvu'); -const assert = require('uvu/assert'); -const execa = require('execa'); +const { test } = require('uvu') +const assert = require('uvu/assert') +const execa = require('execa') test('mocha', async () => { - const proc = await execa('./cli.js', ['mocks/test.mocha.js']); + const proc = await execa('./cli.js', ['mocks/test.mocha.js']) - assert.is(proc.exitCode, 0, 'exit code'); - assert.ok(proc.stdout.includes('5 passing'), 'process stdout'); -}); + assert.is(proc.exitCode, 0, 'exit code') + assert.ok(proc.stdout.includes('5 passing'), 'process stdout') +}) test('mocha with DEBUG=app', async () => { - const proc = await execa('./cli.js', ['mocks/test.mocha.js'], { env: { DEBUG: 'app' } }); + const proc = await execa('./cli.js', ['mocks/test.mocha.js'], { + env: { DEBUG: 'app' }, + }) - assert.is(proc.exitCode, 0, 'exit code'); - assert.ok(proc.stdout.includes('5 passing'), 'process stdout'); - assert.ok(proc.stdout.includes('app test pass'), 'debug output'); -}); + assert.is(proc.exitCode, 0, 'exit code') + assert.ok(proc.stdout.includes('5 passing'), 'process stdout') + assert.ok(proc.stdout.includes('app test pass'), 'debug output') +}) test('mocha incognito', async () => { - const proc = await execa('./cli.js', ['mocks/test.mocha.js', '--incognito']); + const proc = await execa('./cli.js', ['mocks/test.mocha.js', '--incognito']) - assert.is(proc.exitCode, 0, 'exit code'); - assert.ok(proc.stdout.includes('5 passing'), 'process stdout'); -}); + assert.is(proc.exitCode, 0, 'exit code') + assert.ok(proc.stdout.includes('5 passing'), 'process stdout') +}) test('mocha mode:worker', async () => { - const proc = await execa('./cli.js', ['mocks/test.mocha.js', '--mode', 'worker']); + const proc = await execa('./cli.js', [ + 'mocks/test.mocha.js', + '--mode', + 'worker', + ]) - assert.is(proc.exitCode, 0, 'exit code'); - assert.ok(proc.stdout.includes('5 passing'), 'process stdout'); -}); + assert.is(proc.exitCode, 0, 'exit code') + assert.ok(proc.stdout.includes('5 passing'), 'process stdout') +}) -test.skip('mocha extension', async () => { - const proc = await execa('./cli.js', ['mocks/test.mocha.js', '--extension']); +test('mocha extension', async () => { + const proc = await execa('./cli.js', ['mocks/test.mocha.js', '--extension']) - assert.is(proc.exitCode, 0, 'exit code'); - assert.ok(proc.stdout.includes('5 passing'), 'process stdout'); -}); + assert.is(proc.exitCode, 0, 'exit code') + assert.ok(proc.stdout.includes('5 passing'), 'process stdout') +}) test('tape', async () => { - const proc = await execa('./cli.js', ['mocks/test.tape.js', '--runner', 'tape']); + const proc = await execa('./cli.js', [ + 'mocks/test.tape.js', + '--runner', + 'tape', + ]) - assert.is(proc.exitCode, 0, 'exit code'); - assert.ok(proc.stdout.includes('# pass 2'), 'process stdout'); -}); + assert.is(proc.exitCode, 0, 'exit code') + assert.ok(proc.stdout.includes('# pass 5'), 'process stdout') +}) test('tape mode:worker', async () => { - const proc = await execa('./cli.js', ['mocks/test.tape.js', '--runner', 'tape', '--mode', 'worker']); - - assert.is(proc.exitCode, 0, 'exit code'); - assert.ok(proc.stdout.includes('# pass 2'), 'process stdout'); -}); + const proc = await execa('./cli.js', [ + 'mocks/test.tape.js', + '--runner', + 'tape', + '--mode', + 'worker', + ]) + + assert.is(proc.exitCode, 0, 'exit code') + assert.ok(proc.stdout.includes('# pass 5'), 'process stdout') +}) test('zora', async () => { - const proc = await execa('./cli.js', ['mocks/*.zora.js', '--runner', 'zora'], { - env: { - 'RUN_ONLY': true, - 'INDENT': true - } - }); - - assert.is(proc.exitCode, 0, 'exit code'); - assert.ok(proc.stdout.includes('# success: 2'), 'process stdout'); -}); + const proc = await execa( + './cli.js', + ['mocks/*.zora.js', '--runner', 'zora'], + { + env: { + RUN_ONLY: 'true', + INDENT: 'true', + }, + } + ) + + assert.is(proc.exitCode, 0, 'exit code') + assert.ok(proc.stdout.includes('# success: 2'), 'process stdout') +}) test('zora mode:worker', async () => { - const proc = await execa('./cli.js', ['mocks/*.zora.js', '--runner', 'zora', '--mode', 'worker'], { - env: { - 'RUN_ONLY': true, - 'INDENT': true - } - }); - - assert.is(proc.exitCode, 0, 'exit code'); - assert.ok(proc.stdout.includes('# success: 2'), 'process stdout'); -}); + const proc = await execa( + './cli.js', + ['mocks/*.zora.js', '--runner', 'zora', '--mode', 'worker'], + { + env: { + RUN_ONLY: 'true', + INDENT: 'true', + }, + } + ) + + assert.is(proc.exitCode, 0, 'exit code') + assert.ok(proc.stdout.includes('# success: 2'), 'process stdout') +}) test.skip('benchmark', async () => { - const proc = await execa('./cli.js', ['mocks/benchmark.js', '--runner', 'benchmark']); - - assert.is(proc.exitCode, 0, 'exit code'); - assert.ok(proc.stdout.includes('Fastest is String#indexOf'), 'process stdout'); -}); -test.run(); + const proc = await execa('./cli.js', [ + 'mocks/benchmark.js', + '--runner', + 'benchmark', + ]) + + assert.is(proc.exitCode, 0, 'exit code') + assert.ok(proc.stdout.includes('Fastest is String#indexOf'), 'process stdout') +}) +test.run() diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..59c7ab44 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "hd-scripts/tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src", "test.js", "cli.js", "package.json"] +}