├── .husky ├── .gitignore └── pre-commit ├── .npmrc ├── .typos.toml ├── packages ├── supertape │ ├── lib │ │ ├── fixture │ │ │ ├── not-test.js │ │ │ ├── invalid.js │ │ │ ├── cli.js │ │ │ ├── cli-success.js │ │ │ └── help │ │ ├── is-debug.js │ │ ├── exit-codes.spec.mjs │ │ ├── exit-codes.js │ │ ├── supertape.mjs │ │ ├── help.js │ │ ├── supertape.spec.mjs │ │ ├── diff.mjs │ │ ├── format.js │ │ ├── diff.spec.mjs │ │ ├── cli │ │ │ └── parse-args.js │ │ ├── worker │ │ │ ├── create-console-log.js │ │ │ └── create-console-log.spec.js │ │ ├── formatter │ │ │ ├── harness.js │ │ │ ├── index.js │ │ │ └── harness.spec.js │ │ ├── supertape.d.ts │ │ ├── validator.js │ │ ├── format.spec.js │ │ ├── cli.js │ │ ├── run-tests.js │ │ ├── supertape.js │ │ ├── operators.mjs │ │ ├── operators.spec.mjs │ │ └── validator.spec.js │ ├── supertape.json │ ├── .gitignore │ ├── .npmignore │ ├── bin │ │ ├── trace.mjs │ │ ├── is-stop.mjs │ │ ├── supertape.spec.mjs │ │ ├── subscribe.spec.mjs │ │ ├── trace.spec.mjs │ │ ├── tracer.mjs │ │ ├── is-stop.spec.mjs │ │ ├── communication.mjs │ │ ├── supertape.mjs │ │ ├── subscribe.mjs │ │ ├── formatter.mjs │ │ └── formatter.spec.mjs │ ├── .nycrc.json │ ├── eslint.config.mjs │ ├── .putout.json │ ├── help.json │ ├── .madrun.mjs │ ├── LICENSE │ ├── test │ │ └── errors.ts │ └── package.json ├── engine-loader │ ├── eslint.config.js │ ├── .gitignore │ ├── .npmignore │ ├── .putout.json │ ├── lib │ │ ├── simple-import.js │ │ ├── loader.js │ │ ├── loader.spec.js │ │ └── get-paths.js │ ├── .nycrc.json │ ├── .madrun.js │ ├── LICENSE │ ├── README.md │ └── package.json ├── formatter-fail │ ├── eslint.config.js │ ├── .gitignore │ ├── .npmignore │ ├── .putout.json │ ├── .nycrc.json │ ├── lib │ │ ├── fail.js │ │ └── fail.spec.js │ ├── .madrun.js │ ├── README.md │ ├── LICENSE │ └── package.json ├── formatter-short │ ├── eslint.config.js │ ├── .gitignore │ ├── .npmignore │ ├── .putout.json │ ├── .nycrc.json │ ├── .madrun.js │ ├── README.md │ ├── LICENSE │ ├── package.json │ └── lib │ │ ├── short.js │ │ └── short.spec.js ├── formatter-tap │ ├── eslint.config.js │ ├── .gitignore │ ├── .npmignore │ ├── .putout.json │ ├── .nycrc.json │ ├── .madrun.js │ ├── README.md │ ├── LICENSE │ ├── package.json │ └── lib │ │ ├── tap.js │ │ └── tap.spec.js ├── formatter-time │ ├── eslint.config.js │ ├── .gitignore │ ├── .npmignore │ ├── .putout.json │ ├── .nycrc.json │ ├── .madrun.js │ ├── README.md │ ├── LICENSE │ ├── package.json │ └── lib │ │ ├── time.js │ │ └── time.spec.js ├── operator-stub │ ├── eslint.config.js │ ├── .gitignore │ ├── .npmignore │ ├── .putout.json │ ├── .nycrc.json │ ├── .madrun.js │ ├── test │ │ └── errors.ts │ ├── lib │ │ ├── stub.d.ts │ │ └── stub.js │ ├── LICENSE │ ├── package.json │ └── README.md ├── formatter-json-lines │ ├── eslint.config.js │ ├── .gitignore │ ├── .npmignore │ ├── .putout.json │ ├── .nycrc.json │ ├── .madrun.js │ ├── README.md │ ├── LICENSE │ ├── lib │ │ ├── json-lines.js │ │ └── json-lines.spec.js │ └── package.json └── formatter-progress-bar │ ├── eslint.config.js │ ├── .gitignore │ ├── .npmignore │ ├── .putout.json │ ├── .nycrc.json │ ├── .madrun.js │ ├── README.md │ ├── LICENSE │ ├── package.json │ └── lib │ └── progress-bar.js ├── .github ├── FUNDING.yml └── workflows │ ├── nodejs-pr.yml │ └── nodejs.yml ├── .gitignore ├── .npmignore ├── lerna.json ├── .putout.json ├── eslint.config.mjs ├── .nycrc.json ├── LICENSE ├── .madrun.mjs └── package.json /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | [files] 2 | extend-exclude = ["ChangeLog"] 3 | -------------------------------------------------------------------------------- /packages/supertape/lib/fixture/not-test.js: -------------------------------------------------------------------------------- 1 | module.exports = () => 'hi'; 2 | -------------------------------------------------------------------------------- /packages/supertape/supertape.json: -------------------------------------------------------------------------------- 1 | { 2 | "operators": ["stub"] 3 | } 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ./node_modules/.bin/putout --staged -f progress 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: coderaiser 2 | open_collective: cloudcmd 3 | ko_fi: coderaiser 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | 3 | coverage 4 | node_modules 5 | yarn-error.log 6 | 7 | *.swp 8 | .idea 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.spec.js 3 | test 4 | yarn-error.log 5 | 6 | fixture 7 | 8 | coverage 9 | *.config.* 10 | -------------------------------------------------------------------------------- /packages/engine-loader/eslint.config.js: -------------------------------------------------------------------------------- 1 | import {safeAlign} from 'eslint-plugin-putout'; 2 | 3 | export default safeAlign; 4 | -------------------------------------------------------------------------------- /packages/formatter-fail/eslint.config.js: -------------------------------------------------------------------------------- 1 | import {safeAlign} from 'eslint-plugin-putout'; 2 | 3 | export default safeAlign; 4 | -------------------------------------------------------------------------------- /packages/formatter-short/eslint.config.js: -------------------------------------------------------------------------------- 1 | import {safeAlign} from 'eslint-plugin-putout'; 2 | 3 | export default safeAlign; 4 | -------------------------------------------------------------------------------- /packages/formatter-tap/eslint.config.js: -------------------------------------------------------------------------------- 1 | import {safeAlign} from 'eslint-plugin-putout'; 2 | 3 | export default safeAlign; 4 | -------------------------------------------------------------------------------- /packages/formatter-time/eslint.config.js: -------------------------------------------------------------------------------- 1 | import {safeAlign} from 'eslint-plugin-putout'; 2 | 3 | export default safeAlign; 4 | -------------------------------------------------------------------------------- /packages/operator-stub/eslint.config.js: -------------------------------------------------------------------------------- 1 | import {safeAlign} from 'eslint-plugin-putout'; 2 | 3 | export default safeAlign; 4 | -------------------------------------------------------------------------------- /packages/formatter-json-lines/eslint.config.js: -------------------------------------------------------------------------------- 1 | import {safeAlign} from 'eslint-plugin-putout'; 2 | 3 | export default safeAlign; 4 | -------------------------------------------------------------------------------- /packages/supertape/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | node_modules 3 | .nyc_output 4 | yarn-error.log 5 | .*.swp 6 | 7 | coverage 8 | .idea 9 | -------------------------------------------------------------------------------- /packages/engine-loader/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | node_modules 3 | .nyc_output 4 | yarn-error.log 5 | .*.swp 6 | 7 | coverage 8 | .idea 9 | -------------------------------------------------------------------------------- /packages/formatter-fail/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | node_modules 3 | .nyc_output 4 | yarn-error.log 5 | .*.swp 6 | 7 | coverage 8 | .idea 9 | -------------------------------------------------------------------------------- /packages/formatter-progress-bar/eslint.config.js: -------------------------------------------------------------------------------- 1 | import {safeAlign} from 'eslint-plugin-putout'; 2 | 3 | export default safeAlign; 4 | -------------------------------------------------------------------------------- /packages/formatter-short/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | node_modules 3 | .nyc_output 4 | yarn-error.log 5 | .*.swp 6 | 7 | coverage 8 | .idea 9 | -------------------------------------------------------------------------------- /packages/formatter-tap/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | node_modules 3 | .nyc_output 4 | yarn-error.log 5 | .*.swp 6 | 7 | coverage 8 | .idea 9 | -------------------------------------------------------------------------------- /packages/formatter-time/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | node_modules 3 | .nyc_output 4 | yarn-error.log 5 | .*.swp 6 | 7 | coverage 8 | .idea 9 | -------------------------------------------------------------------------------- /packages/operator-stub/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | node_modules 3 | .nyc_output 4 | yarn-error.log 5 | .*.swp 6 | 7 | coverage 8 | .idea 9 | -------------------------------------------------------------------------------- /packages/supertape/.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.spec.* 3 | test 4 | yarn-error.log 5 | coverage 6 | 7 | fixture 8 | 9 | *.config.* 10 | -------------------------------------------------------------------------------- /packages/engine-loader/.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.spec.js 3 | test 4 | yarn-error.log 5 | coverage 6 | 7 | fixture 8 | 9 | *.config.* 10 | -------------------------------------------------------------------------------- /packages/formatter-fail/.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.spec.js 3 | test 4 | yarn-error.log 5 | coverage 6 | 7 | fixture 8 | 9 | *.config.* 10 | -------------------------------------------------------------------------------- /packages/formatter-json-lines/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | node_modules 3 | .nyc_output 4 | yarn-error.log 5 | .*.swp 6 | 7 | coverage 8 | .idea 9 | -------------------------------------------------------------------------------- /packages/formatter-progress-bar/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | node_modules 3 | .nyc_output 4 | yarn-error.log 5 | .*.swp 6 | 7 | coverage 8 | .idea 9 | -------------------------------------------------------------------------------- /packages/formatter-tap/.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.spec.js 3 | test 4 | yarn-error.log 5 | coverage 6 | 7 | fixture 8 | 9 | *.config.* 10 | -------------------------------------------------------------------------------- /packages/formatter-time/.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.spec.js 3 | test 4 | yarn-error.log 5 | coverage 6 | 7 | fixture 8 | 9 | *.config.* 10 | -------------------------------------------------------------------------------- /packages/operator-stub/.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.spec.js 3 | test 4 | yarn-error.log 5 | coverage 6 | 7 | fixture 8 | 9 | *.config.* 10 | -------------------------------------------------------------------------------- /packages/formatter-short/.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.spec.js 3 | test 4 | yarn-error.log 5 | coverage 6 | 7 | fixture 8 | 9 | *.config.* 10 | -------------------------------------------------------------------------------- /packages/formatter-json-lines/.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.spec.js 3 | test 4 | yarn-error.log 5 | coverage 6 | 7 | fixture 8 | 9 | *.config.* 10 | -------------------------------------------------------------------------------- /packages/formatter-progress-bar/.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.spec.js 3 | test 4 | yarn-error.log 5 | coverage 6 | 7 | fixture 8 | 9 | *.config.* 10 | -------------------------------------------------------------------------------- /packages/supertape/bin/trace.mjs: -------------------------------------------------------------------------------- 1 | export const createTrace = (parentPort) => (event, data) => { 2 | parentPort?.postMessage([event, data]); 3 | }; 4 | -------------------------------------------------------------------------------- /packages/engine-loader/.putout.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": { 3 | "*.md": { 4 | "remove-unreachable-code": "off" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/formatter-fail/.putout.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": { 3 | "*.md": { 4 | "remove-unreachable-code": "off" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/formatter-short/.putout.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": { 3 | "*.md": { 4 | "remove-unreachable-code": "off" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/formatter-tap/.putout.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": { 3 | "*.md": { 4 | "remove-unreachable-code": "off" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/formatter-time/.putout.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": { 3 | "*.md": { 4 | "remove-unreachable-code": "off" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/operator-stub/.putout.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": { 3 | "*.md": { 4 | "remove-unreachable-code": "off" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/formatter-json-lines/.putout.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": { 3 | "*.md": { 4 | "remove-unreachable-code": "off" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/supertape/lib/fixture/invalid.js: -------------------------------------------------------------------------------- 1 | const test = require('xxx'); 2 | 3 | test('hello world', (t) => { 4 | t.equal(1, 2); 5 | t.end(); 6 | }); 7 | 8 | -------------------------------------------------------------------------------- /packages/formatter-progress-bar/.putout.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": { 3 | "*.md": { 4 | "remove-unreachable-code": "off" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/supertape/lib/fixture/cli.js: -------------------------------------------------------------------------------- 1 | const test = require('../supertape'); 2 | 3 | test('hello world', (t) => { 4 | t.equal(1, 2); 5 | t.end(); 6 | }); 7 | 8 | -------------------------------------------------------------------------------- /packages/engine-loader/lib/simple-import.js: -------------------------------------------------------------------------------- 1 | // How in other way to mock import using mock require in CommonJS? 2 | export const simpleImport = async (url) => await import(url); 3 | -------------------------------------------------------------------------------- /packages/supertape/lib/fixture/cli-success.js: -------------------------------------------------------------------------------- 1 | const test = require('../supertape'); 2 | 3 | test('hello world: 1', (t) => { 4 | t.equal(1, 1); 5 | t.end(); 6 | }); 7 | 8 | -------------------------------------------------------------------------------- /packages/supertape/lib/is-debug.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const process = require('node:process'); 4 | const argv = process.execArgv.join(); 5 | 6 | module.exports = argv.includes('inspect') || argv.includes('debug'); 7 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "loglevel": "error", 3 | "packages": ["packages/*"], 4 | "npmClient": "yarn", 5 | "npmClientArgs": ["--no-lockfile"], 6 | "useWorkspaces": true, 7 | "version": "independent" 8 | } 9 | -------------------------------------------------------------------------------- /.putout.json: -------------------------------------------------------------------------------- 1 | { 2 | "match": { 3 | "*.md": { 4 | "remove-unreachable-code": "off" 5 | }, 6 | ".filesystem.json": { 7 | "eslint/convert-rc-to-flat": "on" 8 | } 9 | }, 10 | "ignore": [".husky"] 11 | } 12 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import {safeAlign} from 'eslint-plugin-putout'; 2 | import {mergeESLintConfigs} from '@putout/eslint-flat'; 3 | import {defineConfig} from 'eslint/config'; 4 | 5 | const config = await mergeESLintConfigs(import.meta.url, 'packages'); 6 | 7 | export default defineConfig([safeAlign, config]); 8 | -------------------------------------------------------------------------------- /packages/supertape/lib/exit-codes.spec.mjs: -------------------------------------------------------------------------------- 1 | import {test} from 'supertape'; 2 | 3 | test('supertape: exit-codes', async (t) => { 4 | const {SKIPPED} = await import('supertape/exit-codes'); 5 | const Original = await import('./exit-codes.js'); 6 | 7 | t.equal(Original.SKIPPED, SKIPPED); 8 | t.end(); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/engine-loader/.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": true, 3 | "all": true, 4 | "exclude": [ 5 | ".*", 6 | "{bin,lib}/**/{fixture,*.spec.{js,mjs}}", 7 | "**/*.config.*" 8 | ], 9 | "branches": 100, 10 | "lines": 100, 11 | "functions": 100, 12 | "statements": 100 13 | } 14 | -------------------------------------------------------------------------------- /packages/formatter-fail/.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": true, 3 | "all": true, 4 | "exclude": [ 5 | ".*", 6 | "{bin,lib}/**/{fixture,*.spec.{js,mjs}}", 7 | "**/*.config.*" 8 | ], 9 | "branches": 100, 10 | "lines": 100, 11 | "functions": 100, 12 | "statements": 100 13 | } 14 | -------------------------------------------------------------------------------- /packages/formatter-short/.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": true, 3 | "all": true, 4 | "exclude": [ 5 | ".*", 6 | "{bin,lib}/**/{fixture,*.spec.{js,mjs}}", 7 | "**/*.config.*" 8 | ], 9 | "branches": 100, 10 | "lines": 100, 11 | "functions": 100, 12 | "statements": 100 13 | } 14 | -------------------------------------------------------------------------------- /packages/formatter-tap/.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": true, 3 | "all": true, 4 | "exclude": [ 5 | ".*", 6 | "{bin,lib}/**/{fixture,*.spec.{js,mjs}}", 7 | "**/*.config.*" 8 | ], 9 | "branches": 100, 10 | "lines": 100, 11 | "functions": 100, 12 | "statements": 100 13 | } 14 | -------------------------------------------------------------------------------- /packages/formatter-time/.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": true, 3 | "all": true, 4 | "exclude": [ 5 | ".*", 6 | "{bin,lib}/**/{fixture,*.spec.{js,mjs}}", 7 | "**/*.config.*" 8 | ], 9 | "branches": 100, 10 | "lines": 100, 11 | "functions": 100, 12 | "statements": 100 13 | } 14 | -------------------------------------------------------------------------------- /packages/operator-stub/.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": true, 3 | "all": true, 4 | "exclude": [ 5 | ".*", 6 | "{bin,lib}/**/{fixture,*.spec.{js,mjs}}", 7 | "**/*.config.*" 8 | ], 9 | "branches": 100, 10 | "lines": 100, 11 | "functions": 100, 12 | "statements": 100 13 | } 14 | -------------------------------------------------------------------------------- /packages/formatter-json-lines/.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": true, 3 | "all": true, 4 | "exclude": [ 5 | ".*", 6 | "{bin,lib}/**/{fixture,*.spec.{js,mjs}}", 7 | "**/*.config.*" 8 | ], 9 | "branches": 100, 10 | "lines": 100, 11 | "functions": 100, 12 | "statements": 100 13 | } 14 | -------------------------------------------------------------------------------- /packages/formatter-progress-bar/.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": true, 3 | "all": true, 4 | "exclude": [ 5 | ".*", 6 | "{bin,lib}/**/{fixture,*.spec.{js,mjs}}", 7 | "**/*.config.*" 8 | ], 9 | "branches": 100, 10 | "lines": 100, 11 | "functions": 100, 12 | "statements": 100 13 | } 14 | -------------------------------------------------------------------------------- /packages/supertape/lib/exit-codes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const OK = 0; 4 | const FAIL = 1; 5 | const WAS_STOP = 2; 6 | const UNHANDLED = 3; 7 | const INVALID_OPTION = 4; 8 | const SKIPPED = 5; 9 | 10 | module.exports = { 11 | OK, 12 | FAIL, 13 | WAS_STOP, 14 | UNHANDLED, 15 | INVALID_OPTION, 16 | SKIPPED, 17 | }; 18 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": true, 3 | "all": true, 4 | "exclude": [ 5 | "**/lib/**/{fixture,*.spec.{js,mjs}}", 6 | "**/.*", 7 | "**/*.ts", 8 | "**/packages/supertape/bin/*.mjs", 9 | "**/*.config.*" 10 | ], 11 | "branches": 100, 12 | "lines": 100, 13 | "functions": 100, 14 | "statements": 100 15 | } 16 | -------------------------------------------------------------------------------- /packages/supertape/.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": true, 3 | "all": true, 4 | "exclude": [ 5 | ".*", 6 | "{bin,lib}/**/{fixture,*.spec.{js,mjs}}", 7 | "bin/**/*.mjs", 8 | "**/*.config.*", 9 | "**/*.ts" 10 | ], 11 | "branches": 100, 12 | "lines": 100, 13 | "functions": 100, 14 | "statements": 100 15 | } 16 | -------------------------------------------------------------------------------- /packages/supertape/lib/supertape.mjs: -------------------------------------------------------------------------------- 1 | import test from './supertape.js'; 2 | 3 | const { 4 | extend, 5 | stub, 6 | createStream, 7 | init, 8 | run, 9 | createTest, 10 | } = test; 11 | 12 | export default test; 13 | 14 | export { 15 | extend, 16 | test, 17 | stub, 18 | createStream, 19 | init, 20 | run, 21 | createTest, 22 | }; 23 | -------------------------------------------------------------------------------- /packages/supertape/lib/help.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.help = () => { 4 | const bin = require('../help.json'); 5 | const usage = 'Usage: supertape [options] [path]'; 6 | const result = [usage, 'Options']; 7 | 8 | for (const name of Object.keys(bin)) { 9 | result.push(` ${name} ${bin[name]}`); 10 | } 11 | 12 | return result.join('\n') + '\n'; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/supertape/bin/is-stop.mjs: -------------------------------------------------------------------------------- 1 | import fullstore from 'fullstore'; 2 | 3 | const noop = () => {}; 4 | 5 | export const createIsStop = (parentPort) => { 6 | if (!parentPort) 7 | return noop; 8 | 9 | const isStop = fullstore(false); 10 | 11 | parentPort?.on('message', ([event]) => { 12 | if (event === 'stop') 13 | isStop(true); 14 | }); 15 | 16 | return isStop; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/supertape/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'eslint/config'; 2 | import {safeAlign} from 'eslint-plugin-putout'; 3 | import {matchToFlat} from '@putout/eslint-flat'; 4 | 5 | export const match = { 6 | '*.d.ts': { 7 | 'no-var': 'off', 8 | }, 9 | '*.spec.*': { 10 | 'node/no-extraneous-import': 'off', 11 | }, 12 | }; 13 | 14 | export default defineConfig([safeAlign, matchToFlat(match)]); 15 | -------------------------------------------------------------------------------- /packages/supertape/bin/supertape.spec.mjs: -------------------------------------------------------------------------------- 1 | import {createRequire} from 'node:module'; 2 | import runsome from 'runsome'; 3 | import test from '../lib/supertape.js'; 4 | 5 | const require = createRequire(import.meta.url); 6 | 7 | const name = new URL('tracer.mjs', import.meta.url).pathname; 8 | const run = runsome(name); 9 | 10 | test('supertape: bin: -v', (t) => { 11 | const {version} = require('../package.json'); 12 | 13 | t.equal(run('-v'), `v${version}`); 14 | t.end(); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/supertape/.putout.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "nodejs/declare": ["on", { 4 | "dismiss": ["setTimeout"] 5 | }] 6 | }, 7 | "match": { 8 | "*.md": { 9 | "remove-unreachable-code": "off" 10 | }, 11 | "test/errors.ts": { 12 | "tape/remove-only": "off", 13 | "tape/remove-skip": "off" 14 | }, 15 | "*.js": { 16 | "nodejs/convert-esm-to-commonjs": "on" 17 | } 18 | }, 19 | "ignore": ["test"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/formatter-fail/lib/fail.js: -------------------------------------------------------------------------------- 1 | import * as tap from '@supertape/formatter-tap'; 2 | 3 | export { 4 | start, 5 | comment, 6 | end, 7 | } from '@supertape/formatter-tap'; 8 | 9 | import fullstore from 'fullstore'; 10 | 11 | const testStore = fullstore(); 12 | 13 | export const test = ({test}) => { 14 | testStore(test); 15 | return ''; 16 | }; 17 | 18 | export const fail = (...a) => { 19 | const message = testStore(); 20 | const fail = tap.fail(...a); 21 | 22 | return `# ${message}\n${fail}`; 23 | }; 24 | -------------------------------------------------------------------------------- /packages/formatter-tap/.madrun.js: -------------------------------------------------------------------------------- 1 | import {run} from 'madrun'; 2 | 3 | export default { 4 | 'test': () => `supertape 'lib/**/*.spec.js'`, 5 | 'watch:test': async () => `nodemon -w lib -w test -x "${await run('test')}"`, 6 | 'lint': () => 'putout .', 7 | 'fresh:lint': () => run('lint', '--fresh'), 8 | 'lint:fresh': () => run('lint', '--fresh'), 9 | 'fix:lint': () => run('lint', '--fix'), 10 | 'coverage': () => `c8 npm test`, 11 | 'report': () => 'c8 report --reporter=lcov', 12 | 'wisdom': () => run(['lint', 'coverage']), 13 | }; 14 | -------------------------------------------------------------------------------- /packages/formatter-fail/.madrun.js: -------------------------------------------------------------------------------- 1 | import {run} from 'madrun'; 2 | 3 | export default { 4 | 'test': () => `supertape 'lib/**/*.spec.js'`, 5 | 'watch:test': async () => `nodemon -w lib -w test -x "${await run('test')}"`, 6 | 'lint': () => 'putout .', 7 | 'fresh:lint': () => run('lint', '--fresh'), 8 | 'lint:fresh': () => run('lint', '--fresh'), 9 | 'fix:lint': () => run('lint', '--fix'), 10 | 'coverage': () => `c8 npm test`, 11 | 'report': () => 'c8 report --reporter=lcov', 12 | 'wisdom': () => run(['lint', 'coverage']), 13 | }; 14 | -------------------------------------------------------------------------------- /packages/formatter-short/.madrun.js: -------------------------------------------------------------------------------- 1 | import {run} from 'madrun'; 2 | 3 | export default { 4 | 'test': () => `supertape 'lib/**/*.spec.js'`, 5 | 'watch:test': async () => `nodemon -w lib -w test -x "${await run('test')}"`, 6 | 'lint': () => 'putout .', 7 | 'fresh:lint': () => run('lint', '--fresh'), 8 | 'lint:fresh': () => run('lint', '--fresh'), 9 | 'fix:lint': () => run('lint', '--fix'), 10 | 'coverage': () => `c8 npm test`, 11 | 'report': () => 'c8 report --reporter=lcov', 12 | 'wisdom': () => run(['lint', 'coverage']), 13 | }; 14 | -------------------------------------------------------------------------------- /packages/formatter-tap/README.md: -------------------------------------------------------------------------------- 1 | # @supertape/formatter-tap [![NPM version][NPMIMGURL]][NPMURL] 2 | 3 | [NPMIMGURL]: https://img.shields.io/npm/v/@supertape/formatter-tap.svg?style=flat&longCache=true 4 | [NPMURL]: https://npmjs.org/package/@supertape/formatter-tap "npm" 5 | 6 | 📼[`Supertape`](https://github.com/coderaiser/supertape) formatter shows progress bar. 7 | 8 | ## Install 9 | 10 | ``` 11 | npm i supertape @supertape/formatter-tap 12 | ``` 13 | 14 | ## Usage 15 | 16 | ``` 17 | supertape --format tap lib 18 | ``` 19 | 20 | ## License 21 | 22 | MIT 23 | -------------------------------------------------------------------------------- /packages/formatter-time/.madrun.js: -------------------------------------------------------------------------------- 1 | import {run} from 'madrun'; 2 | 3 | export default { 4 | 'test': () => `supertape 'lib/**/*.spec.js'`, 5 | 'watch:test': async () => `nodemon -w lib -w test -x "${await run('test')}"`, 6 | 'lint': () => 'putout .', 7 | 'fresh:lint': () => run('lint', '--fresh'), 8 | 'lint:fresh': () => run('lint', '--fresh'), 9 | 'fix:lint': () => run('lint', '--fix'), 10 | 'coverage': () => `c8 npm test`, 11 | 'report': () => 'c8 report --reporter=lcov', 12 | 'wisdom': () => run(['lint', 'coverage']), 13 | }; 14 | -------------------------------------------------------------------------------- /packages/formatter-fail/README.md: -------------------------------------------------------------------------------- 1 | # @supertape/formatter-fail [![NPM version][NPMIMGURL]][NPMURL] 2 | 3 | [NPMIMGURL]: https://img.shields.io/npm/v/@supertape/formatter-fail.svg?style=flat&longCache=true 4 | [NPMURL]: https://npmjs.org/package/@supertape/formatter-fail "npm" 5 | 6 | 📼[`Supertape`](https://github.com/coderaiser/supertape) formatter shows only failed. 7 | 8 | ## Install 9 | 10 | ``` 11 | npm i supertape @supertape/formatter-fail 12 | ``` 13 | 14 | ## Usage 15 | 16 | ``` 17 | supertape --format fail lib 18 | ``` 19 | 20 | ## License 21 | 22 | MIT 23 | -------------------------------------------------------------------------------- /packages/formatter-json-lines/.madrun.js: -------------------------------------------------------------------------------- 1 | import {run} from 'madrun'; 2 | 3 | export default { 4 | 'test': () => `supertape 'lib/**/*.spec.js'`, 5 | 'watch:test': async () => `nodemon -w lib -w test -x "${await run('test')}"`, 6 | 'lint': () => 'putout .', 7 | 'fresh:lint': () => run('lint', '--fresh'), 8 | 'lint:fresh': () => run('lint', '--fresh'), 9 | 'fix:lint': () => run('lint', '--fix'), 10 | 'coverage': () => `c8 npm test`, 11 | 'report': () => 'c8 report --reporter=lcov', 12 | 'wisdom': () => run(['lint', 'coverage']), 13 | }; 14 | -------------------------------------------------------------------------------- /packages/formatter-progress-bar/.madrun.js: -------------------------------------------------------------------------------- 1 | import {run} from 'madrun'; 2 | 3 | export default { 4 | 'test': () => `supertape 'lib/**/*.spec.js'`, 5 | 'watch:test': async () => `nodemon -w lib -w test -x "${await run('test')}"`, 6 | 'lint': () => 'putout .', 7 | 'fresh:lint': () => run('lint', '--fresh'), 8 | 'lint:fresh': () => run('lint', '--fresh'), 9 | 'fix:lint': () => run('lint', '--fix'), 10 | 'coverage': () => `c8 npm test`, 11 | 'report': () => 'c8 report --reporter=lcov', 12 | 'wisdom': () => run(['lint', 'coverage']), 13 | }; 14 | -------------------------------------------------------------------------------- /packages/engine-loader/.madrun.js: -------------------------------------------------------------------------------- 1 | import {run} from 'madrun'; 2 | 3 | export default { 4 | 'test': () => `supertape 'lib/**/*.spec.js'`, 5 | 'watch:test': async () => `nodemon -w lib -w test -x "${await run('test')}"`, 6 | 'lint': () => 'putout .', 7 | 'fresh:lint': () => run('lint', '--fresh'), 8 | 'lint:fresh': () => run('lint', '--fresh'), 9 | 'fix:lint': () => run('lint', '--fix'), 10 | 'coverage': async () => `c8 ${await run('test')}`, 11 | 'report': () => 'c8 report --reporter=lcov', 12 | 'wisdom': () => run(['lint', 'coverage']), 13 | }; 14 | -------------------------------------------------------------------------------- /packages/formatter-short/README.md: -------------------------------------------------------------------------------- 1 | # @supertape/formatter-short [![NPM version][NPMIMGURL]][NPMURL] 2 | 3 | [NPMIMGURL]: https://img.shields.io/npm/v/@supertape/formatter-short.svg?style=flat&longCache=true 4 | [NPMURL]: https://npmjs.org/package/@supertape/formatter-short "npm" 5 | 6 | 📼[`Supertape`](https://github.com/coderaiser/supertape) formatter with minimalistic output. 7 | 8 | ## Install 9 | 10 | ``` 11 | npm i supertape @supertape/formatter-short 12 | ``` 13 | 14 | ## Usage 15 | 16 | ``` 17 | supertape --format short lib 18 | ``` 19 | 20 | ## License 21 | 22 | MIT 23 | -------------------------------------------------------------------------------- /packages/operator-stub/.madrun.js: -------------------------------------------------------------------------------- 1 | import {run} from 'madrun'; 2 | 3 | export default { 4 | 'test': () => `supertape 'lib/**/*.spec.js'`, 5 | 'test:dts': () => 'check-dts', 6 | 'watch:test': async () => `nodemon -w lib -w test -x "${await run('test')}"`, 7 | 'lint': () => 'putout .', 8 | 'fresh:lint': () => run('lint', '--fresh'), 9 | 'lint:fresh': () => run('lint', '--fresh'), 10 | 'fix:lint': () => run('lint', '--fix'), 11 | 'coverage': async () => `c8 ${await run('test')}`, 12 | 'report': () => 'c8 report --reporter=lcov', 13 | 'wisdom': () => run(['lint', 'coverage', 'test:dts']), 14 | }; 15 | -------------------------------------------------------------------------------- /packages/supertape/lib/supertape.spec.mjs: -------------------------------------------------------------------------------- 1 | import test, {extend, stub} from './supertape.mjs'; 2 | 3 | const extendedTest = extend({ 4 | superOk: (operator) => (a) => { 5 | return operator.ok(a, 'should be super ok'); 6 | }, 7 | }); 8 | 9 | test('supertape: mjs: default: equal', (t) => { 10 | t.equal(1, 1); 11 | t.end(); 12 | }); 13 | 14 | test('supertape: mjs: default: calledWith', (t) => { 15 | const fn = stub(); 16 | 17 | fn('hello'); 18 | 19 | t.calledWith(fn, ['hello']); 20 | t.end(); 21 | }); 22 | 23 | extendedTest('supertape: mjs: extend', (t) => { 24 | t.superOk(true); 25 | t.end(); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/supertape/lib/fixture/help: -------------------------------------------------------------------------------- 1 | Usage: supertape [options] [path] 2 | Options 3 | -h, --help display this help and exit 4 | -v, --version output version information and exit 5 | -f, --format use a specific output format - default: progress-bar/tap on CI 6 | -r, --require require module 7 | --no-check-scopes do not check that messages contains scope: 'scope: message' 8 | --no-check-assertions-count do not check that assertion count is no more then 1 9 | --no-check-duplicates do not check messages for duplicates 10 | --no-worker disable worker thread 11 | -------------------------------------------------------------------------------- /packages/supertape/help.json: -------------------------------------------------------------------------------- 1 | { 2 | "-h, --help ": "display this help and exit", 3 | "-v, --version ": "output version information and exit", 4 | "-f, --format ": "use a specific output format - default: progress-bar/tap on CI", 5 | "-r, --require ": "require module", 6 | "--no-check-scopes ": "do not check that messages contains scope: 'scope: message'", 7 | "--no-check-assertions-count": "do not check that assertion count is no more then 1", 8 | "--no-check-duplicates ": "do not check messages for duplicates", 9 | "--no-worker ": "disable worker thread" 10 | } 11 | -------------------------------------------------------------------------------- /packages/engine-loader/lib/loader.js: -------------------------------------------------------------------------------- 1 | import {pathToFileURL} from 'node:url'; 2 | import {getPaths} from './get-paths.js'; 3 | import {simpleImport} from './simple-import.js'; 4 | 5 | const {assign} = Object; 6 | 7 | export const loadOperators = async (operators) => { 8 | const promises = []; 9 | const paths = getPaths(operators); 10 | 11 | for (const path of paths) { 12 | // always convert to fileURL for windows 13 | const resolved = pathToFileURL(path); 14 | promises.push(simpleImport(resolved)); 15 | } 16 | 17 | const loadedOperators = await Promise.all(promises); 18 | 19 | return assign(...loadedOperators); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/engine-loader/lib/loader.spec.js: -------------------------------------------------------------------------------- 1 | import {test} from 'supertape'; 2 | import tryToCatch from 'try-to-catch'; 3 | import {loadOperators} from './loader.js'; 4 | 5 | test('supertape: engine: loader', async (t) => { 6 | const result = await loadOperators(['stub']); 7 | 8 | const expected = await import('@supertape/operator-stub'); 9 | 10 | t.deepEqual(result, expected); 11 | t.end(); 12 | }); 13 | 14 | test('supertape: engine: loader: not found', async (t) => { 15 | const [error] = await tryToCatch(loadOperators, ['not-found']); 16 | 17 | const expected = 'Operator "supertape-operator-not-found" could not be found!'; 18 | 19 | t.equal(error.message, expected); 20 | t.end(); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/supertape/bin/subscribe.spec.mjs: -------------------------------------------------------------------------------- 1 | import {test, stub} from 'supertape'; 2 | import {consoleLog, consoleError} from './subscribe.mjs'; 3 | 4 | test('supertype: subscribe: consoleLog', (t) => { 5 | const log = stub(); 6 | const logger = { 7 | log, 8 | }; 9 | 10 | consoleLog({ 11 | message: '', 12 | logger, 13 | }); 14 | 15 | t.calledWith(log, ['']); 16 | t.end(); 17 | }); 18 | 19 | test('supertype: subscribe: consoleError', (t) => { 20 | const error = stub(); 21 | const logger = { 22 | error, 23 | }; 24 | 25 | consoleError({ 26 | message: '', 27 | logger, 28 | }); 29 | 30 | t.calledWith(error, ['']); 31 | t.end(); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/supertape/.madrun.mjs: -------------------------------------------------------------------------------- 1 | import {run, cutEnv} from 'madrun'; 2 | 3 | const env = { 4 | SUPERTAPE_PROGRESS_BAR_STACK: 0, 5 | }; 6 | 7 | export default { 8 | 'test': () => [env, `bin/tracer.mjs '{bin,lib}/**/*.spec.{js,mjs}'`], 9 | 'test:dts': () => 'check-dts test/*.ts', 10 | 'watch:test': async () => `nodemon -w lib -w test -x "${await cutEnv('test')}"`, 11 | 'lint': () => 'putout .', 12 | 'fresh:lint': () => run('lint', '--fresh'), 13 | 'lint:fresh': () => run('lint', '--fresh'), 14 | 'fix:lint': () => run('lint', '--fix'), 15 | 'coverage': async () => [env, `c8 ${await cutEnv('test')}`], 16 | 'report': () => 'c8 report --reporter=lcov', 17 | 'wisdom': () => run(['lint', 'test:dts', 'coverage']), 18 | }; 19 | -------------------------------------------------------------------------------- /packages/supertape/lib/diff.mjs: -------------------------------------------------------------------------------- 1 | import {stripVTControlCharacters} from 'node:util'; 2 | import {diff} from 'jest-diff'; 3 | import {formatOutput, addSpaces} from './format.js'; 4 | 5 | export default (a, b) => { 6 | const diffed = diff(a, b) 7 | .replaceAll('Object ', '') 8 | .replaceAll('Array ', ''); 9 | 10 | let striped = diffed; 11 | 12 | if (diffed.includes('\n')) 13 | striped = diffed 14 | .split('\n') 15 | .slice(3) 16 | .join('\n'); 17 | 18 | if (stripVTControlCharacters(diffed) === 'Compared values have no visual difference.') 19 | return ''; 20 | 21 | const output = [ 22 | addSpaces('diff: |-'), 23 | formatOutput(striped), 24 | ]; 25 | 26 | return output.join('\n'); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/supertape/bin/trace.spec.mjs: -------------------------------------------------------------------------------- 1 | import {test, stub} from 'supertape'; 2 | import tryCatch from 'try-catch'; 3 | import {createTrace} from './trace.mjs'; 4 | 5 | test('supertape: bin: trace: parentPort', (t) => { 6 | const data = {}; 7 | const postMessage = stub(); 8 | const parentPort = { 9 | postMessage, 10 | }; 11 | 12 | const trace = createTrace(parentPort); 13 | 14 | trace('start', data); 15 | 16 | const args = [ 17 | ['start', data], 18 | ]; 19 | 20 | t.calledWith(postMessage, args); 21 | t.end(); 22 | }); 23 | 24 | test('supertape: bin: trace: no parentPort', (t) => { 25 | const data = {}; 26 | const trace = createTrace(); 27 | const [error] = tryCatch(trace, 'start', data); 28 | 29 | t.notOk(error); 30 | t.end(); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/supertape/bin/tracer.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import process from 'node:process'; 4 | import {Worker} from 'node:worker_threads'; 5 | import {parseArgs} from '../lib/cli/parse-args.js'; 6 | import {subscribe} from './subscribe.mjs'; 7 | 8 | const { 9 | cwd, 10 | exit, 11 | stdout, 12 | } = process; 13 | 14 | const args = parseArgs(process.argv.slice(2)); 15 | const write = stdout.write.bind(stdout); 16 | 17 | if (!args.worker) { 18 | await import('./supertape.mjs'); 19 | exit(); 20 | } 21 | 22 | const slave = new URL('./supertape.mjs', import.meta.url); 23 | 24 | const worker = new Worker(slave, { 25 | workerData: process.argv, 26 | stdin: true, 27 | }); 28 | 29 | await subscribe({ 30 | name: args.format, 31 | args, 32 | worker, 33 | exit, 34 | cwd, 35 | write, 36 | stdout, 37 | }); 38 | -------------------------------------------------------------------------------- /packages/formatter-json-lines/README.md: -------------------------------------------------------------------------------- 1 | # @supertape/formatter-json-lines [![NPM version][NPMIMGURL]][NPMURL] 2 | 3 | [NPMIMGURL]: https://img.shields.io/npm/v/@supertape/formatter-json-lines.svg?style=flat&longCache=true 4 | [NPMURL]: https://npmjs.org/package/@supertape/formatter-json-lines "npm" 5 | 6 | 📼[`Supertape`](https://github.com/coderaiser/supertape) formatter shows [json-lines](https://jsonlines.org/) 7 | 8 | ## Install 9 | 10 | ``` 11 | npm i supertape @supertape/formatter-json-lines 12 | ``` 13 | 14 | ## Usage 15 | 16 | ``` 17 | supertape --format json-lines lib 18 | 19 | {"count":1,"total":3,"failed":0,"test":"supertape: format: json-lines"} 20 | {"count":2,"total":3,"failed":0,"test":"supertape: format: json-lines: skip"} 21 | {"count":3,"total":3,"failed":0,"test":"supertape: format: json-lines: comment"} 22 | {"count":3,"passed":3,"failed":0,"skipped":0} 23 | ``` 24 | 25 | ## License 26 | 27 | MIT 28 | -------------------------------------------------------------------------------- /packages/formatter-time/README.md: -------------------------------------------------------------------------------- 1 | # @supertape/formatter-time [![NPM version][NPMIMGURL]][NPMURL] 2 | 3 | [NPMIMGURL]: https://img.shields.io/npm/v/@supertape/formatter-time.svg?style=flat&longCache=true 4 | [NPMURL]: https://npmjs.org/package/@supertape/formatter-time "npm" 5 | 6 | 📼[`Supertape`](https://github.com/coderaiser/supertape) formatter shows progress bar. 7 | 8 | ## Install 9 | 10 | ``` 11 | npm i supertape @supertape/formatter-time 12 | ``` 13 | 14 | ## Usage 15 | 16 | ``` 17 | supertape --format time lib 18 | ``` 19 | 20 | ## Env Variables 21 | 22 | - `CI=1` - disable progress bar 23 | - `SUPERTAPE_TIME=1` - force enable/disable progress bar; 24 | - `SUPERTAPE_TIME_COLOR` - set color of progress bar; 25 | - `SUPERTAPE_TIME_MIN=100` - count of tests to show progress bar; 26 | - `SUPERTAPE_TIME_STACK=1` - force show/hide stack on fail; 27 | - `SUPERTAPE_TIME_CLOCK=⏳` - set clock icon; 28 | 29 | ## License 30 | 31 | MIT 32 | -------------------------------------------------------------------------------- /packages/formatter-progress-bar/README.md: -------------------------------------------------------------------------------- 1 | # @supertape/formatter-progress-bar [![NPM version][NPMIMGURL]][NPMURL] 2 | 3 | [NPMIMGURL]: https://img.shields.io/npm/v/@supertape/formatter-progress-bar.svg?style=flat&longCache=true 4 | [NPMURL]: https://npmjs.org/package/@supertape/formatter-progress-bar "npm" 5 | 6 | 📼[`Supertape`](https://github.com/coderaiser/supertape) formatter shows progress bar. 7 | 8 | ## Install 9 | 10 | ``` 11 | npm i supertape @supertape/formatter-progress-bar 12 | ``` 13 | 14 | ## Usage 15 | 16 | ``` 17 | supertape --format progress-bar lib 18 | ``` 19 | 20 | ## Env Variables 21 | 22 | - `CI=1` - disable progress bar 23 | - `SUPERTAPE_PROGRESS_BAR=1` - force enable/disable progress bar; 24 | - `SUPERTAPE_PROGRESS_BAR_COLOR` - set color of progress bar; 25 | - `SUPERTAPE_PROGRESS_BAR_MIN=100` - count of tests to show progress bar; 26 | - `SUPERTAPE_PROGRESS_BAR_STACK=1` - force show/hide stack on fail; 27 | 28 | ## License 29 | 30 | MIT 31 | -------------------------------------------------------------------------------- /packages/operator-stub/test/errors.ts: -------------------------------------------------------------------------------- 1 | import {stub, Stub} from '@cloudcmd/stub'; 2 | import {test, Test} from 'supertape'; 3 | 4 | const a: string = stub(); 5 | 6 | const fn: Stub = stub(); 7 | fn(a); 8 | 9 | const fn1: Stub = stub(); 10 | const fn2: Stub = stub(); 11 | 12 | test('calledWith', (t: Test) => { 13 | t.calledWith(fn, [a]); 14 | t.end(); 15 | }); 16 | 17 | test('calledWith', (t: Test) => { 18 | t.calledWith(fn, [a]); 19 | t.end(); 20 | }); 21 | 22 | test('calledAfter', (t: Test) => { 23 | t.calledAfter(fn1, fn2); 24 | t.end(); 25 | }); 26 | 27 | test('calledBefore', (t: Test) => { 28 | t.calledBefore(fn1, fn2); 29 | t.end(); 30 | }); 31 | 32 | test('calledInOrder', (t: Test) => { 33 | t.calledInOrder([fn1, fn2]); 34 | t.end(); 35 | }); 36 | 37 | test('calledInOrder: not stub', (t: Test) => { 38 | // THROWS Type 'FunctionConstructor' is not assignable to type 'Stub'. 39 | t.calledInOrder([Function]); 40 | t.end(); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/supertape/lib/format.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const addSpaces = (a) => ` ${a}`; 4 | 5 | const REASON_USER = 3; 6 | const REASON_EXCEPTION = 1; 7 | 8 | module.exports.parseAt = (stack, {reason}) => { 9 | const lines = cutMockImport(stack).split('\n'); 10 | 11 | if (lines.length === 1) 12 | return stack; 13 | 14 | if (lines.length > 10 && lines[0].startsWith('Error: ')) 15 | return lines[0]; 16 | 17 | const line = lines[reason === 'user' ? REASON_USER : REASON_EXCEPTION]; 18 | 19 | if (!line) 20 | throw Error(`☝️ Looks like 'async' operator called without 'await': ${stack}`); 21 | 22 | return line.trim(); 23 | }; 24 | 25 | module.exports.addSpaces = addSpaces; 26 | module.exports.formatOutput = (str) => { 27 | return str 28 | .split('\n') 29 | .map(addSpaces) 30 | .join('\n'); 31 | }; 32 | 33 | function cutMockImport(str) { 34 | return str.replace(/\?mock-import-count=\d+/g, ''); 35 | } 36 | -------------------------------------------------------------------------------- /packages/supertape/bin/is-stop.spec.mjs: -------------------------------------------------------------------------------- 1 | import {test, stub} from 'supertape'; 2 | import {createIsStop} from './is-stop.mjs'; 3 | 4 | test('putout: bin: isStop: parentPort', (t) => { 5 | const on = stub(); 6 | const parentPort = { 7 | on, 8 | }; 9 | 10 | const args = ['message', stub()]; 11 | 12 | createIsStop(parentPort); 13 | 14 | t.calledWith(on, args); 15 | t.end(); 16 | }); 17 | 18 | test('putout: bin: isStop: no parentPort', (t) => { 19 | const isStop = createIsStop(); 20 | const result = isStop(); 21 | 22 | t.notOk(result); 23 | t.end(); 24 | }); 25 | 26 | test('putout: bin: isStop: parentPort: isStop', (t) => { 27 | const on = stub(); 28 | const parentPort = { 29 | on, 30 | }; 31 | 32 | const isStop = createIsStop(parentPort); 33 | const [args] = on.args; 34 | const [, listener] = args; 35 | 36 | listener(['stop']); 37 | 38 | const result = isStop(); 39 | 40 | t.ok(result); 41 | t.end(); 42 | }); 43 | -------------------------------------------------------------------------------- /packages/engine-loader/lib/get-paths.js: -------------------------------------------------------------------------------- 1 | import {createRequire} from 'node:module'; 2 | import tryCatch from 'try-catch'; 3 | 4 | const {resolve} = createRequire(import.meta.url); 5 | 6 | const bigFirst = (a) => `${a[0].toUpperCase()}${a.slice(1)}`; 7 | 8 | const namespace = 'supertape'; 9 | const type = 'operator'; 10 | 11 | export const getPaths = (operators) => { 12 | const names = []; 13 | 14 | for (const name of operators) { 15 | const fullPath = getPath(namespace, type, name); 16 | 17 | if (!fullPath) 18 | throw Error(`${bigFirst(type)} "${namespace}-${type}-${name}" could not be found!`); 19 | 20 | names.push(fullPath); 21 | } 22 | 23 | return names; 24 | }; 25 | 26 | function getPath(namespace, type, name) { 27 | let path = getModulePath(`@${namespace}/${type}-${name}`); 28 | 29 | if (!path) 30 | path = getModulePath(`${namespace}-${type}-${name}`); 31 | 32 | return path; 33 | } 34 | 35 | function getModulePath(name) { 36 | const [, path] = tryCatch(resolve, name); 37 | return path; 38 | } 39 | -------------------------------------------------------------------------------- /packages/operator-stub/lib/stub.d.ts: -------------------------------------------------------------------------------- 1 | import {Stub} from '@cloudcmd/stub'; 2 | 3 | type OperationResult = { 4 | is: boolean; 5 | expected: unknown; 6 | actual: unknown; 7 | message: string; 8 | }; 9 | 10 | export function stub(arg?: unknown): Stub; 11 | export interface OperatorStub { 12 | called: (fn: Stub, message?: string) => OperationResult; 13 | notCalled: (fn: Stub, message?: string) => OperationResult; 14 | calledWith: (fn: Stub, args: unknown[], message?: string) => OperationResult; 15 | calledWithNoArgs: (fn: Stub, message?: string) => OperationResult; 16 | calledCount: (fn: Stub, count: number, message?: string) => OperationResult; 17 | calledOnce: (fn: Stub, message?: string) => OperationResult; 18 | calledTwice: (fn: Stub, message?: string) => OperationResult; 19 | calledWithNew: (fn: Stub, message?: string) => OperationResult; 20 | calledBefore: (fn1: Stub, fn2: Stub, message?: string) => OperationResult; 21 | calledAfter: (fn1: Stub, fn2: Stub, message?: string) => OperationResult; 22 | calledInOrder: (fns: Stub[], message?: string) => OperationResult; 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) coderaiser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/engine-loader/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) coderaiser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/formatter-tap/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) coderaiser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/operator-stub/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) coderaiser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/supertape/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) coderaiser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/formatter-fail/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) coderaiser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/formatter-short/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) coderaiser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/formatter-time/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) coderaiser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/formatter-json-lines/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) coderaiser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/formatter-progress-bar/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) coderaiser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/supertape/bin/communication.mjs: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'node:events'; 2 | import {parentPort, workerData} from 'node:worker_threads'; 3 | 4 | const {assign} = Object; 5 | const returns = (a) => () => a; 6 | 7 | export const createCommunication = (argv) => { 8 | if (parentPort) 9 | return { 10 | parentPort, 11 | workerData, 12 | isMaster: returns(false), 13 | }; 14 | 15 | const {newWorker, newParentPort} = fakeWorkers(); 16 | 17 | return { 18 | worker: newWorker, 19 | parentPort: newParentPort, 20 | workerData: argv, 21 | isMaster: returns(true), 22 | }; 23 | }; 24 | 25 | export function fakeWorkers() { 26 | const newWorker = new EventEmitter(); 27 | const newParentPort = new EventEmitter(); 28 | 29 | assign(newWorker, { 30 | postMessage: (a) => { 31 | newParentPort.emit('message', a); 32 | }, 33 | }); 34 | 35 | assign(newParentPort, { 36 | postMessage: (a) => { 37 | newWorker.emit('message', a); 38 | }, 39 | }); 40 | 41 | return { 42 | newParentPort, 43 | newWorker, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /packages/engine-loader/README.md: -------------------------------------------------------------------------------- 1 | # @supertape/engine-loader [![NPM version][NPMIMGURL]][NPMURL] [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Coverage Status][CoverageIMGURL]][CoverageURL] 2 | 3 | [NPMIMGURL]: https://img.shields.io/npm/v/supertape.svg?style=flat&longCache=true 4 | [BuildStatusIMGURL]: https://img.shields.io/travis/coderaiser/supertape/master.svg?style=flat&longCache=true 5 | [NPMURL]: https://npmjs.org/package/supertape "npm" 6 | [BuildStatusURL]: https://travis-ci.org/coderaiser/supertape "Build Status" 7 | [CoverageURL]: https://coveralls.io/github/coderaiser/supertape?branch=master 8 | [CoverageIMGURL]: https://coveralls.io/repos/coderaiser/supertape/badge.svg?branch=master&service=github 9 | 10 | Load operators into `supertape`. 11 | 12 | ## Install 13 | 14 | ``` 15 | npm i @supertape/engine-loader -D 16 | ``` 17 | 18 | ## Examples 19 | 20 | ```js 21 | import {loadOperators} from '@supertape/engine-loader'; 22 | import {extend, stub} from 'supertape'; 23 | 24 | const operators = await loadOperators(['stub']); 25 | 26 | const test = extend(operators); 27 | 28 | test('with operators', (t) => { 29 | const fn = stub(); 30 | 31 | fn('hello'); 32 | 33 | t.calledWith(fn, ['hello']); 34 | t.end(); 35 | }); 36 | ``` 37 | 38 | ## License 39 | 40 | MIT 41 | -------------------------------------------------------------------------------- /packages/supertape/lib/diff.spec.mjs: -------------------------------------------------------------------------------- 1 | import {stripVTControlCharacters} from 'node:util'; 2 | import test from './supertape.js'; 3 | import diff from './diff.mjs'; 4 | 5 | const noop = () => {}; 6 | 7 | test('supertape: diff', (t) => { 8 | const diffed = diff(undefined, 'hello'); 9 | const {length} = diffed.split('\n'); 10 | const expected = 2; 11 | 12 | t.equal(length, expected); 13 | t.end(); 14 | }); 15 | 16 | test('supertape: diff: no Array', (t) => { 17 | const result = stripVTControlCharacters(diff(['hello'], [])); 18 | const expected = ` 19 | diff: |- 20 | - [ 21 | - "hello", 22 | - ] 23 | + [] 24 | `.slice(1, -5); 25 | 26 | t.equal(result, expected); 27 | t.end(); 28 | }); 29 | 30 | test('supertape: diff: no Object', (t) => { 31 | const result = stripVTControlCharacters(diff({a: 'b'}, {})); 32 | 33 | const expected = ` 34 | diff: |- 35 | - { 36 | - "a": "b", 37 | - } 38 | + {} 39 | `.slice(1, -5); 40 | 41 | t.equal(result, expected); 42 | t.end(); 43 | }); 44 | 45 | test('supertape: diff: no diff', (t) => { 46 | const a = { 47 | fn: noop, 48 | }; 49 | 50 | const b = { 51 | fn: noop, 52 | }; 53 | 54 | const diffed = diff(a, b); 55 | 56 | t.notOk(diffed); 57 | t.end(); 58 | }); 59 | -------------------------------------------------------------------------------- /packages/formatter-tap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supertape/formatter-tap", 3 | "version": "4.0.0", 4 | "author": "coderaiser (https://github.com/coderaiser)", 5 | "description": "📼Supertape formatter tap", 6 | "homepage": "http://github.com/coderaiser/supertape", 7 | "main": "./lib/tap.js", 8 | "release": false, 9 | "tag": false, 10 | "changelog": false, 11 | "type": "module", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/coderaiser/supertape.git" 15 | }, 16 | "scripts": { 17 | "test": "madrun test", 18 | "watch:test": "madrun watch:test", 19 | "lint": "madrun lint", 20 | "fix:lint": "madrun fix:lint", 21 | "coverage": "madrun coverage", 22 | "report": "madrun report", 23 | "wisdom": "madrun wisdom" 24 | }, 25 | "dependencies": {}, 26 | "keywords": [ 27 | "formatter", 28 | "tap", 29 | "supertape" 30 | ], 31 | "devDependencies": { 32 | "c8": "^10.1.2", 33 | "eslint": "^9.1.1", 34 | "eslint-plugin-putout": "^29.0.2", 35 | "madrun": "^11.0.0", 36 | "montag": "^1.0.0", 37 | "nodemon": "^3.0.1", 38 | "pullout": "^5.0.1", 39 | "putout": "^41.0.2", 40 | "supertape": "^11.0.0" 41 | }, 42 | "license": "MIT", 43 | "engines": { 44 | "node": ">=20" 45 | }, 46 | "publishConfig": { 47 | "access": "public" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/supertape/bin/supertape.mjs: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import {createCommunication} from './communication.mjs'; 3 | import {subscribe} from './subscribe.mjs'; 4 | import {createIsStop} from './is-stop.mjs'; 5 | import {createFormatter} from './formatter.mjs'; 6 | import {parseArgs} from '../lib/cli/parse-args.js'; 7 | import cli from '../lib/cli.js'; 8 | import { 9 | overrideConsoleError, 10 | overrideConsoleLog, 11 | } from '../lib/worker/create-console-log.js'; 12 | 13 | const { 14 | worker, 15 | parentPort, 16 | workerData, 17 | isMaster, 18 | } = createCommunication(process.argv); 19 | 20 | const args = parseArgs(process.argv.slice(2)); 21 | 22 | const { 23 | stdout, 24 | stderr, 25 | exit, 26 | } = process; 27 | 28 | const workerFormatter = createFormatter(parentPort); 29 | const isStop = createIsStop(parentPort); 30 | 31 | if (isMaster()) { 32 | subscribe({ 33 | name: args.format, 34 | exit, 35 | worker, 36 | stdout, 37 | }); 38 | } else { 39 | overrideConsoleLog(parentPort, { 40 | console, 41 | }); 42 | 43 | overrideConsoleError(parentPort, { 44 | console, 45 | }); 46 | } 47 | 48 | export default await cli({ 49 | stdout, 50 | stderr, 51 | exit, 52 | cwd: process.cwd(), 53 | argv: workerData.slice(2), 54 | workerFormatter, 55 | isStop, 56 | }); 57 | -------------------------------------------------------------------------------- /packages/formatter-short/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supertape/formatter-short", 3 | "version": "3.0.0", 4 | "author": "coderaiser (https://github.com/coderaiser)", 5 | "description": "📼Supertape formatter with short output (without callstack)", 6 | "homepage": "http://github.com/coderaiser/supertape", 7 | "main": "./lib/short.js", 8 | "release": false, 9 | "tag": false, 10 | "changelog": false, 11 | "type": "module", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/coderaiser/supertape.git" 15 | }, 16 | "scripts": { 17 | "test": "madrun test", 18 | "watch:test": "madrun watch:test", 19 | "lint": "madrun lint", 20 | "fix:lint": "madrun fix:lint", 21 | "coverage": "madrun coverage", 22 | "report": "madrun report", 23 | "wisdom": "madrun wisdom" 24 | }, 25 | "dependencies": {}, 26 | "keywords": [ 27 | "formatter", 28 | "tap", 29 | "supertape" 30 | ], 31 | "devDependencies": { 32 | "c8": "^10.1.2", 33 | "eslint": "^9.1.1", 34 | "eslint-plugin-putout": "^29.0.2", 35 | "madrun": "^11.0.0", 36 | "montag": "^1.0.0", 37 | "nodemon": "^3.0.1", 38 | "pullout": "^5.0.1", 39 | "putout": "^41.0.2", 40 | "supertape": "^11.0.0" 41 | }, 42 | "license": "MIT", 43 | "engines": { 44 | "node": ">=20" 45 | }, 46 | "publishConfig": { 47 | "access": "public" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/formatter-json-lines/lib/json-lines.js: -------------------------------------------------------------------------------- 1 | import fullstore from 'fullstore'; 2 | 3 | const out = createOutput(); 4 | const store = fullstore(); 5 | 6 | export const test = ({test}) => { 7 | store(test); 8 | }; 9 | 10 | const stringify = (a) => JSON.stringify(a) + '\n'; 11 | 12 | export const testEnd = ({count, total, failed, test}) => { 13 | return stringify({ 14 | count, 15 | total, 16 | failed, 17 | test, 18 | }); 19 | }; 20 | 21 | export const fail = ({at, count, message, operator, result, expected, output, errorStack}) => { 22 | return out({ 23 | test: store(), 24 | at, 25 | count, 26 | message, 27 | operator, 28 | result, 29 | expected, 30 | errorStack, 31 | output, 32 | }); 33 | }; 34 | 35 | export const end = ({count, passed, failed, skipped}) => { 36 | out({ 37 | count, 38 | passed, 39 | failed, 40 | skipped, 41 | }); 42 | 43 | return out(); 44 | }; 45 | 46 | function createOutput() { 47 | let output = []; 48 | 49 | return (...args) => { 50 | const [line] = args; 51 | 52 | if (!args.length) { 53 | const result = output.join(''); 54 | 55 | output = []; 56 | 57 | return result; 58 | } 59 | 60 | output.push(stringify(line)); 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /packages/formatter-json-lines/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supertape/formatter-json-lines", 3 | "version": "2.0.1", 4 | "author": "coderaiser (https://github.com/coderaiser)", 5 | "description": "📼Supertape formatter progress bar", 6 | "homepage": "http://github.com/coderaiser/supertape", 7 | "main": "./lib/json-lines.js", 8 | "release": false, 9 | "tag": false, 10 | "changelog": false, 11 | "type": "module", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/coderaiser/supertape.git" 15 | }, 16 | "scripts": { 17 | "test": "madrun test", 18 | "watch:test": "madrun watch:test", 19 | "lint": "madrun lint", 20 | "fix:lint": "madrun fix:lint", 21 | "coverage": "madrun coverage", 22 | "report": "madrun report", 23 | "wisdom": "madrun wisdom" 24 | }, 25 | "dependencies": { 26 | "fullstore": "^3.0.0" 27 | }, 28 | "keywords": [ 29 | "formatter", 30 | "json-lines", 31 | "supertape" 32 | ], 33 | "devDependencies": { 34 | "c8": "^10.1.2", 35 | "eslint": "^9.1.1", 36 | "eslint-plugin-putout": "^29.0.2", 37 | "madrun": "^11.0.0", 38 | "montag": "^1.0.0", 39 | "nodemon": "^3.0.1", 40 | "pullout": "^5.0.1", 41 | "putout": "^41.0.2", 42 | "supertape": "^11.0.0" 43 | }, 44 | "license": "MIT", 45 | "engines": { 46 | "node": ">=20" 47 | }, 48 | "publishConfig": { 49 | "access": "public" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/nodejs-pr.yml: -------------------------------------------------------------------------------- 1 | name: Node PR CI 2 | on: pull_request 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | env: 7 | NAME: supertape 8 | FORCE_COLOR: 3 9 | strategy: 10 | matrix: 11 | node-version: 12 | - 20.x 13 | - 22.x 14 | - 24.x 15 | - 25.x 16 | steps: 17 | - uses: actions/checkout@v5 18 | - uses: oven-sh/setup-bun@v2 19 | with: 20 | bun-version: latest 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v6 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - name: Install Redrun 26 | run: bun i redrun -g --no-save 27 | - name: Install 28 | run: bun i --no-save 29 | - name: Bootstrap 30 | run: redrun bootstrap 31 | - name: Lint 32 | run: redrun lint 33 | - name: Install Rust 34 | run: rustup update 35 | - uses: actions/cache@v4 36 | with: 37 | path: | 38 | ~/.cargo/bin/ 39 | ~/.cargo/registry/index/ 40 | ~/.cargo/registry/cache/ 41 | ~/.cargo/git/db/ 42 | target/ 43 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 44 | - name: Typos Install 45 | run: cargo install typos-cli || echo 'already installed' 46 | - name: Typos 47 | run: typos 48 | - name: Coverage 49 | run: redrun coverage report 50 | -------------------------------------------------------------------------------- /packages/formatter-fail/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supertape/formatter-fail", 3 | "version": "4.0.0", 4 | "author": "coderaiser (https://github.com/coderaiser)", 5 | "description": "📼Supertape formatter shows only failed tests", 6 | "homepage": "http://github.com/coderaiser/supertape", 7 | "main": "./lib/fail.js", 8 | "release": false, 9 | "tag": false, 10 | "changelog": false, 11 | "type": "module", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/coderaiser/supertape.git" 15 | }, 16 | "scripts": { 17 | "test": "madrun test", 18 | "watch:test": "madrun watch:test", 19 | "lint": "madrun lint", 20 | "fix:lint": "madrun fix:lint", 21 | "coverage": "madrun coverage", 22 | "report": "madrun report", 23 | "wisdom": "madrun wisdom" 24 | }, 25 | "dependencies": { 26 | "@supertape/formatter-tap": "^4.0.0", 27 | "fullstore": "^3.0.0" 28 | }, 29 | "keywords": [ 30 | "formatter", 31 | "fail", 32 | "supertape" 33 | ], 34 | "devDependencies": { 35 | "c8": "^10.1.2", 36 | "eslint": "^9.1.1", 37 | "eslint-plugin-putout": "^29.0.2", 38 | "madrun": "^11.0.0", 39 | "montag": "^1.0.0", 40 | "nodemon": "^3.0.1", 41 | "pullout": "^5.0.1", 42 | "putout": "^41.0.2", 43 | "supertape": "^11.0.0" 44 | }, 45 | "license": "MIT", 46 | "engines": { 47 | "node": ">=20" 48 | }, 49 | "publishConfig": { 50 | "access": "public" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/engine-loader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supertape/engine-loader", 3 | "version": "2.0.0", 4 | "author": "coderaiser (https://github.com/coderaiser)", 5 | "description": "supertape stub operator", 6 | "homepage": "http://github.com/coderaiser/supertape", 7 | "main": "./lib/loader.js", 8 | "release": false, 9 | "tag": false, 10 | "changelog": false, 11 | "type": "module", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/coderaiser/supertape.git" 15 | }, 16 | "scripts": { 17 | "test": "madrun test", 18 | "watch:test": "madrun watch:test", 19 | "lint": "madrun lint", 20 | "fix:lint": "madrun fix:lint", 21 | "coverage": "madrun coverage", 22 | "report": "madrun report", 23 | "wisdom": "madrun wisdom" 24 | }, 25 | "dependencies": { 26 | "try-catch": "^3.0.0" 27 | }, 28 | "keywords": [ 29 | "operator", 30 | "function", 31 | "tap", 32 | "tape", 33 | "testing" 34 | ], 35 | "devDependencies": { 36 | "@supertape/operator-stub": "*", 37 | "c8": "^10.1.2", 38 | "eslint": "^9.1.1", 39 | "eslint-plugin-putout": "^29.0.2", 40 | "madrun": "^11.0.0", 41 | "mock-require": "^3.0.2", 42 | "nodemon": "^3.0.1", 43 | "putout": "^41.0.2", 44 | "supertape": "^11.0.0", 45 | "try-to-catch": "^3.0.0" 46 | }, 47 | "license": "MIT", 48 | "engines": { 49 | "node": ">=16" 50 | }, 51 | "publishConfig": { 52 | "access": "public" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/formatter-progress-bar/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supertape/formatter-progress-bar", 3 | "version": "7.0.0", 4 | "author": "coderaiser (https://github.com/coderaiser)", 5 | "description": "📼 Supertape formatter progress bar", 6 | "homepage": "http://github.com/coderaiser/supertape", 7 | "main": "./lib/progress-bar.js", 8 | "release": false, 9 | "tag": false, 10 | "changelog": false, 11 | "type": "module", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/coderaiser/supertape.git" 15 | }, 16 | "scripts": { 17 | "test": "madrun test", 18 | "watch:test": "madrun watch:test", 19 | "lint": "madrun lint", 20 | "fix:lint": "madrun fix:lint", 21 | "coverage": "madrun coverage", 22 | "report": "madrun report", 23 | "wisdom": "madrun wisdom" 24 | }, 25 | "dependencies": { 26 | "chalk": "^5.3.0", 27 | "ci-info": "^4.0.0", 28 | "cli-progress": "^3.8.2", 29 | "fullstore": "^3.0.0", 30 | "once": "^1.4.0" 31 | }, 32 | "keywords": [ 33 | "formatter", 34 | "progress-bar", 35 | "supertape" 36 | ], 37 | "devDependencies": { 38 | "c8": "^10.1.2", 39 | "eslint": "^9.1.1", 40 | "eslint-plugin-putout": "^29.0.2", 41 | "madrun": "^11.0.0", 42 | "montag": "^1.0.0", 43 | "nodemon": "^3.0.1", 44 | "pullout": "^5.0.1", 45 | "putout": "^41.0.2", 46 | "supertape": "^11.0.0" 47 | }, 48 | "license": "MIT", 49 | "engines": { 50 | "node": ">=20" 51 | }, 52 | "publishConfig": { 53 | "access": "public" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/formatter-time/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supertape/formatter-time", 3 | "version": "2.0.0", 4 | "author": "coderaiser (https://github.com/coderaiser)", 5 | "description": "📼 Supertape formatter progress bar", 6 | "homepage": "http://github.com/coderaiser/supertape", 7 | "main": "./lib/time.js", 8 | "release": false, 9 | "tag": false, 10 | "changelog": false, 11 | "type": "module", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/coderaiser/supertape.git" 15 | }, 16 | "scripts": { 17 | "test": "madrun test", 18 | "watch:test": "madrun watch:test", 19 | "lint": "madrun lint", 20 | "fix:lint": "madrun fix:lint", 21 | "coverage": "madrun coverage", 22 | "report": "madrun report", 23 | "wisdom": "madrun wisdom" 24 | }, 25 | "dependencies": { 26 | "chalk": "^5.3.0", 27 | "ci-info": "^4.0.0", 28 | "cli-progress": "^3.8.2", 29 | "fullstore": "^3.0.0", 30 | "once": "^1.4.0", 31 | "timer-node": "^5.0.7" 32 | }, 33 | "keywords": [ 34 | "formatter", 35 | "time", 36 | "supertape" 37 | ], 38 | "devDependencies": { 39 | "c8": "^10.1.2", 40 | "eslint": "^9.1.1", 41 | "eslint-plugin-putout": "^29.0.2", 42 | "madrun": "^11.0.0", 43 | "montag": "^1.0.0", 44 | "nodemon": "^3.0.1", 45 | "pullout": "^5.0.1", 46 | "putout": "^41.0.2", 47 | "supertape": "^11.0.0" 48 | }, 49 | "license": "MIT", 50 | "engines": { 51 | "node": ">=20" 52 | }, 53 | "publishConfig": { 54 | "access": "public" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.madrun.mjs: -------------------------------------------------------------------------------- 1 | import {run} from 'madrun'; 2 | 3 | const dirs = ['packages']; 4 | 5 | export default { 6 | 'test': () => `tape --check-scopes --check-assertions-count '${dirs}/*/test/*.{js,mjs}' '${dirs}/*/{bin,lib}/**/*.spec.{js,mjs}'`, 7 | 'test:tap': () => `tape '${dirs}/*/test/*.{js,mjs}' '${dirs}/*/lib/**/*.spec.{js,mjs}'`, 8 | 'test:fail': async () => `"${await run('test')}" -f fail`, 9 | 'test:slow': () => 'lerna run test', 10 | 'coverage:long': async () => `c8 ${await run('test')}`, 11 | 'coverage': async () => `c8 ${await run('test')}`, 12 | 'coverage:tap': async () => `c8 ${await run('test:tap')}`, 13 | 'coverage:slow': () => 'lerna run coverage', 14 | 'lint:slow': () => 'lerna run --no-bail lint', 15 | 'lint-all': async () => `MADRUN_NAME=1 ${await run('lint:*')}`, 16 | 'lint:frame': () => run('lint:ci', '-f codeframe'), 17 | 'lint:fresh': () => run('lint', '--fresh'), 18 | 'lint:memory': () => run('lint:fresh', '-f memory'), 19 | 'fresh:lint': () => run('lint:fresh'), 20 | 'fresh': () => run(['lint:memory', 'coverage']), 21 | 'lint': () => `putout . --rulesdir rules`, 22 | 'prelint': () => 'redlint scan', 23 | 'lint:mark': () => 'putout **/*.md', 24 | 'memory': () => run('lint:fresh', '-f memory'), 25 | 'fix:lint': () => run('lint', '--fix'), 26 | 'fix:lint:fresh': () => run('fix:lint', '--fresh'), 27 | 'fix:lint:cache': () => run('lint:cache', '--fix'), 28 | 'fix:lint:slow': () => 'lerna run --no-bail fix:lint', 29 | 'bootstrap': () => 'npm i', 30 | 'report': () => `c8 report --reporter=lcov`, 31 | 'prepare': () => 'husky', 32 | }; 33 | -------------------------------------------------------------------------------- /packages/supertape/lib/cli/parse-args.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const process = require('node:process'); 4 | const yargsParser = require('yargs-parser'); 5 | const {isArray} = Array; 6 | const maybeFirst = (a) => isArray(a) ? a.pop() : a; 7 | const maybeArray = (a) => isArray(a) ? a : [a]; 8 | 9 | const { 10 | SUPERTAPE_CHECK_DUPLICATES = '1', 11 | SUPERTAPE_CHECK_SCOPES = '1', 12 | SUPERTAPE_CHECK_ASSERTIONS_COUNT = '1', 13 | } = process.env; 14 | 15 | const yargsOptions = { 16 | configuration: { 17 | 'strip-aliased': true, 18 | 'strip-dashed': true, 19 | }, 20 | coerce: { 21 | require: maybeArray, 22 | format: maybeFirst, 23 | }, 24 | string: [ 25 | 'format', 26 | 'require', 27 | ], 28 | boolean: [ 29 | 'version', 30 | 'help', 31 | 'check-duplicates', 32 | 'check-scopes', 33 | 'check-assertions-count', 34 | 'worker', 35 | ], 36 | alias: { 37 | version: 'v', 38 | format: 'f', 39 | help: 'h', 40 | require: 'r', 41 | checkDuplicates: 'd', 42 | checkScopes: 's', 43 | checkAssertionsCount: 'a', 44 | }, 45 | default: { 46 | format: 'progress-bar', 47 | require: [], 48 | checkDuplicates: SUPERTAPE_CHECK_DUPLICATES !== '0', 49 | checkScopes: SUPERTAPE_CHECK_SCOPES !== '0', 50 | checkAssertionsCount: SUPERTAPE_CHECK_ASSERTIONS_COUNT !== '0', 51 | worker: true, 52 | }, 53 | }; 54 | 55 | module.exports.yargsOptions = yargsOptions; 56 | module.exports.parseArgs = (argv) => yargsParser(argv, yargsOptions); 57 | -------------------------------------------------------------------------------- /packages/operator-stub/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supertape/operator-stub", 3 | "version": "3.1.0", 4 | "author": "coderaiser (https://github.com/coderaiser)", 5 | "description": "supertape stub operator", 6 | "homepage": "http://github.com/coderaiser/supertape", 7 | "main": "./lib/stub.js", 8 | "release": false, 9 | "tag": false, 10 | "changelog": false, 11 | "type": "module", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/coderaiser/supertape.git" 15 | }, 16 | "scripts": { 17 | "test": "madrun test", 18 | "test:dts": "madrun test:dts", 19 | "watch:test": "madrun watch:test", 20 | "lint": "madrun lint", 21 | "fresh:lint": "madrun fresh:lint", 22 | "lint:fresh": "madrun lint:fresh", 23 | "fix:lint": "madrun fix:lint", 24 | "coverage": "madrun coverage", 25 | "report": "madrun report", 26 | "wisdom": "madrun wisdom" 27 | }, 28 | "dependencies": { 29 | "@cloudcmd/stub": "^4.0.0" 30 | }, 31 | "keywords": [ 32 | "operator", 33 | "stub", 34 | "function", 35 | "tap", 36 | "tape", 37 | "testing" 38 | ], 39 | "devDependencies": { 40 | "c8": "^10.1.2", 41 | "check-dts": "^0.9.0", 42 | "eslint": "^9.1.1", 43 | "eslint-plugin-putout": "^29.0.2", 44 | "madrun": "^11.0.0", 45 | "mock-require": "^3.0.2", 46 | "nodemon": "^3.0.1", 47 | "putout": "^41.0.2", 48 | "supertape": "^11.0.0", 49 | "typescript": "^5.1.6" 50 | }, 51 | "license": "MIT", 52 | "engines": { 53 | "node": ">=16" 54 | }, 55 | "publishConfig": { 56 | "access": "public" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/supertape/lib/worker/create-console-log.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {stringify} = require('flatted'); 4 | const isError = (a) => a instanceof Error; 5 | const SPLITTER = '>[supertape-splitter]<'; 6 | const CONSOLE_LOG = 'console:log'; 7 | const CONSOLE_ERROR = 'console:error'; 8 | 9 | module.exports.CONSOLE_ERROR = CONSOLE_ERROR; 10 | module.exports.CONSOLE_LOG = CONSOLE_LOG; 11 | module.exports.SPLITTER = SPLITTER; 12 | 13 | module.exports.overrideConsoleLog = (parentPort, {console = global.console} = {}) => { 14 | const {log} = console; 15 | 16 | console.log = createConsoleMethod(CONSOLE_LOG, parentPort); 17 | return { 18 | getBackConsoleLog: () => { 19 | console.log = log; 20 | }, 21 | }; 22 | }; 23 | 24 | module.exports.overrideConsoleError = (parentPort, {console = global.console} = {}) => { 25 | const {error} = console; 26 | 27 | console.error = createConsoleMethod(CONSOLE_ERROR, parentPort); 28 | return { 29 | getBackConsoleError: () => { 30 | console.error = error; 31 | }, 32 | }; 33 | }; 34 | 35 | const createConsoleMethod = (type, parentPort) => (...messages) => { 36 | const prepared = []; 37 | 38 | for (const message of messages) { 39 | if (isError(message)) { 40 | prepared.push(stringify(message.toString())); 41 | continue; 42 | } 43 | 44 | prepared.push(stringify(message)); 45 | } 46 | 47 | parentPort.postMessage([ 48 | type, { 49 | message: prepared.join(SPLITTER), 50 | }, 51 | ]); 52 | }; 53 | 54 | module.exports._createConsoleLog = createConsoleMethod; 55 | -------------------------------------------------------------------------------- /packages/supertape/bin/subscribe.mjs: -------------------------------------------------------------------------------- 1 | import {keypress} from '@putout/cli-keypress'; 2 | import {parse} from 'flatted'; 3 | import harnessCreator from '../lib/formatter/harness.js'; 4 | import { 5 | SPLITTER, 6 | CONSOLE_LOG, 7 | CONSOLE_ERROR, 8 | } from '../lib/worker/create-console-log.js'; 9 | 10 | const one = (fn) => (a) => fn(a); 11 | 12 | const maybeParse = (a) => a && parse(a); 13 | const {createHarness} = harnessCreator; 14 | const resolveFormatter = async (name) => await import(`@supertape/formatter-${name}`); 15 | 16 | export async function subscribe({name, exit, worker, stdout}) { 17 | const {isStop} = keypress(); 18 | const harness = createHarness(await resolveFormatter(name)); 19 | 20 | harness.pipe(stdout); 21 | 22 | worker.on('exit', (code) => { 23 | exit(code); 24 | }); 25 | 26 | worker.on('message', ([type, a]) => { 27 | if (type === CONSOLE_LOG) 28 | return consoleLog(a); 29 | 30 | if (type === CONSOLE_ERROR) 31 | return consoleError(a); 32 | 33 | harness.write({ 34 | type, 35 | ...a, 36 | }); 37 | 38 | if (isStop()) 39 | worker.postMessage(['stop']); 40 | }); 41 | } 42 | 43 | export function consoleLog({message, logger = console}) { 44 | const messages = message 45 | .split(SPLITTER) 46 | .map(one(maybeParse)); 47 | 48 | logger.log(...messages); 49 | } 50 | 51 | export function consoleError({message, logger = console}) { 52 | const messages = message 53 | .split(SPLITTER) 54 | .map(one(maybeParse)); 55 | 56 | logger.error(...messages); 57 | } 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "supertape", 3 | "private": true, 4 | "type": "commonjs", 5 | "scripts": { 6 | "test": "madrun test", 7 | "test:dts": "madrun test:dts", 8 | "test:tap": "madrun test:tap", 9 | "test:fail": "madrun test:fail", 10 | "test:slow": "madrun test:slow", 11 | "coverage:long": "madrun coverage:long", 12 | "coverage": "madrun coverage", 13 | "coverage:tap": "madrun coverage:tap", 14 | "coverage:slow": "madrun coverage:slow", 15 | "lint:slow": "madrun lint:slow", 16 | "lint-all": "madrun lint-all", 17 | "lint:frame": "madrun lint:frame", 18 | "lint:fresh": "madrun lint:fresh", 19 | "lint:memory": "madrun lint:memory", 20 | "fresh:lint": "madrun fresh:lint", 21 | "fresh": "madrun fresh", 22 | "lint": "madrun lint", 23 | "lint:mark": "madrun lint:mark", 24 | "memory": "madrun memory", 25 | "fix:lint": "madrun fix:lint", 26 | "fix:lint:fresh": "madrun fix:lint:fresh", 27 | "fix:lint:cache": "madrun fix:lint:cache", 28 | "fix:lint:slow": "madrun fix:lint:slow", 29 | "bootstrap": "madrun bootstrap", 30 | "report": "madrun report", 31 | "prepare": "madrun prepare" 32 | }, 33 | "devDependencies": { 34 | "@putout/eslint-flat": "^3.0.0", 35 | "c8": "^10.1.2", 36 | "check-dts": "^0.9.0", 37 | "eslint": "^9.1.1", 38 | "eslint-plugin-putout": "^29.0.2", 39 | "husky": "^9.0.11", 40 | "madrun": "^11.0.0", 41 | "putout": "^41.0.2", 42 | "redlint": "^4.0.0", 43 | "supertape": "^11.0.0", 44 | "typescript": "^5.1.6" 45 | }, 46 | "engines": { 47 | "node": ">=20" 48 | }, 49 | "workspaces": [ 50 | "packages/*" 51 | ], 52 | "dependencies": {} 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | on: 3 | push: 4 | branches: master 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | env: 9 | NAME: supertape 10 | FORCE_COLOR: 3 11 | strategy: 12 | matrix: 13 | node-version: 14 | - 20.x 15 | - 22.x 16 | - 24.x 17 | - 25.x 18 | steps: 19 | - uses: actions/checkout@v5 20 | - uses: oven-sh/setup-bun@v2 21 | with: 22 | bun-version: latest 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v6 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - name: Install Redrun 28 | run: bun i redrun -g --no-save 29 | - name: Install 30 | run: bun i --no-save 31 | - name: Lint 32 | run: redrun fix:lint 33 | - name: Install Rust 34 | run: rustup update 35 | - uses: actions/cache@v4 36 | with: 37 | path: | 38 | ~/.cargo/bin/ 39 | ~/.cargo/registry/index/ 40 | ~/.cargo/registry/cache/ 41 | ~/.cargo/git/db/ 42 | target/ 43 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 44 | - name: Typos Install 45 | run: cargo install typos-cli || echo 'already installed' 46 | - name: Typos 47 | run: typos --write-changes 48 | - name: Commit fixes 49 | uses: EndBug/add-and-commit@v9 50 | continue-on-error: true 51 | with: 52 | message: "chore: ${{ env.NAME }}: actions: lint ☘️" 53 | - name: Coverage 54 | run: redrun coverage report 55 | - name: Coveralls 56 | uses: coverallsapp/github-action@v2 57 | continue-on-error: true 58 | with: 59 | github-token: ${{ secrets.GITHUB_TOKEN }} 60 | -------------------------------------------------------------------------------- /packages/formatter-short/lib/short.js: -------------------------------------------------------------------------------- 1 | const isStr = (a) => typeof a === 'string'; 2 | 3 | export const start = () => 'TAP version 13\n'; 4 | 5 | export const test = ({test}) => { 6 | return `# ${test}\n`; 7 | }; 8 | 9 | export const comment = ({message}) => { 10 | return `# ${message}\n`; 11 | }; 12 | 13 | export const success = ({count, message}) => { 14 | return `ok ${count} ${message}\n`; 15 | }; 16 | 17 | export const fail = ({at, count, message, operator, result, expected, output}) => { 18 | const out = createOutput(); 19 | 20 | out(`not ok ${count} ${message}`); 21 | out(' ---'); 22 | out(` operator: ${operator}`); 23 | 24 | if (output) 25 | out(output); 26 | 27 | if (!isStr(output)) { 28 | out(' expected: |-'); 29 | out(` ${expected}`); 30 | out(' result: |-'); 31 | out(` ${result}`); 32 | } 33 | 34 | out(` ${at}`); 35 | out(' ...'); 36 | out(''); 37 | 38 | return out(); 39 | }; 40 | 41 | export const end = ({count, passed, failed, skipped}) => { 42 | const out = createOutput(); 43 | 44 | out(''); 45 | 46 | out(`1..${count}`); 47 | out(`# tests ${count}`); 48 | out(`# pass ${passed}`); 49 | 50 | if (skipped) 51 | out(`# skip ${skipped}`); 52 | 53 | if (failed) 54 | out(`# fail ${failed}`); 55 | 56 | out(''); 57 | 58 | if (!failed) { 59 | out('# ok'); 60 | out(''); 61 | } 62 | 63 | out(''); 64 | 65 | return out(); 66 | }; 67 | 68 | function createOutput() { 69 | const output = []; 70 | 71 | return (...args) => { 72 | const [line] = args; 73 | 74 | if (!args.length) 75 | return output.join('\n'); 76 | 77 | output.push(line); 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /packages/supertape/bin/formatter.mjs: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'node:events'; 2 | import {JSONStringify} from 'json-with-bigint'; 3 | 4 | export const createFormatter = (parentPort) => { 5 | const formatter = new EventEmitter(); 6 | 7 | formatter.on('start', ({total}) => { 8 | parentPort.postMessage(['start', { 9 | total, 10 | }]); 11 | }); 12 | 13 | formatter.on('test', ({test}) => { 14 | parentPort.postMessage(['test', { 15 | test, 16 | }]); 17 | }); 18 | 19 | formatter.on('test:end', ({count, total, failed, test}) => { 20 | parentPort.postMessage(['test:end', { 21 | total, 22 | count, 23 | failed, 24 | test, 25 | }]); 26 | }); 27 | 28 | formatter.on('comment', ({message}) => { 29 | parentPort.postMessage(['comment', { 30 | message, 31 | }]); 32 | }); 33 | 34 | formatter.on('test:success', ({count, message}) => { 35 | parentPort.postMessage(['success', { 36 | count, 37 | message, 38 | }]); 39 | }); 40 | 41 | formatter.on('test:fail', ({at, count, message, operator, result, expected, output, errorStack}) => { 42 | parentPort.postMessage(['fail', { 43 | at, 44 | count, 45 | message, 46 | operator, 47 | result: JSONStringify(result), 48 | expected: JSONStringify(expected), 49 | output, 50 | errorStack, 51 | }]); 52 | }); 53 | 54 | formatter.on('end', ({count, passed, failed, skipped}) => { 55 | parentPort.postMessage(['end', { 56 | count, 57 | passed, 58 | failed, 59 | skipped, 60 | }]); 61 | }); 62 | 63 | return formatter; 64 | }; 65 | -------------------------------------------------------------------------------- /packages/formatter-tap/lib/tap.js: -------------------------------------------------------------------------------- 1 | const isStr = (a) => typeof a === 'string'; 2 | 3 | export const start = () => 'TAP version 13\n'; 4 | 5 | export const test = ({test}) => { 6 | return `# ${test}\n`; 7 | }; 8 | 9 | export const comment = ({message}) => { 10 | return `# ${message}\n`; 11 | }; 12 | 13 | export const success = ({count, message}) => { 14 | return `ok ${count} ${message}\n`; 15 | }; 16 | 17 | export const fail = ({at, count, message, operator, result, expected, output, errorStack}) => { 18 | const out = createOutput(); 19 | 20 | out(`not ok ${count} ${message}`); 21 | out(' ---'); 22 | out(` operator: ${operator}`); 23 | 24 | if (output) 25 | out(output); 26 | 27 | if (!isStr(output)) { 28 | out(' expected: |-'); 29 | out(` ${expected}`); 30 | out(' result: |-'); 31 | out(` ${result}`); 32 | } 33 | 34 | out(` ${at}`); 35 | out(' stack: |-'); 36 | out(errorStack); 37 | out(' ...'); 38 | out(''); 39 | 40 | return out(); 41 | }; 42 | 43 | export const end = ({count, passed, failed, skipped}) => { 44 | const out = createOutput(); 45 | 46 | out(''); 47 | 48 | out(`1..${count}`); 49 | out(`# tests ${count}`); 50 | out(`# pass ${passed}`); 51 | 52 | if (skipped) 53 | out(`# skip ${skipped}`); 54 | 55 | if (failed) 56 | out(`# fail ${failed}`); 57 | 58 | out(''); 59 | 60 | if (!failed) { 61 | out('# ok'); 62 | out(''); 63 | } 64 | 65 | out(''); 66 | 67 | return out(); 68 | }; 69 | 70 | function createOutput() { 71 | const output = []; 72 | 73 | return (...args) => { 74 | const [line] = args; 75 | 76 | if (!args.length) 77 | return output.join('\n'); 78 | 79 | output.push(line); 80 | }; 81 | } 82 | -------------------------------------------------------------------------------- /packages/supertape/lib/formatter/harness.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {Transform: _Transform} = require('node:stream'); 4 | 5 | const {assign} = Object; 6 | 7 | module.exports.createHarness = (reporter, {Transform = _Transform} = {}) => { 8 | const prepared = prepare(reporter); 9 | const stream = new Transform({ 10 | writableObjectMode: true, 11 | 12 | transform(chunk, encoding, callback) { 13 | const {type, ...data} = chunk; 14 | const result = run(prepared, type, data); 15 | 16 | if (this._ended) 17 | return callback(Error(`☝️ Looks like 'async' operator called without 'await'`)); 18 | 19 | if (result) 20 | this.push(result); 21 | 22 | if (type === 'end') { 23 | this._ended = true; 24 | this.push(null); 25 | } 26 | 27 | callback(); 28 | }, 29 | }); 30 | 31 | return stream; 32 | }; 33 | 34 | const stub = () => {}; 35 | 36 | function prepare(reporter) { 37 | const result = {}; 38 | 39 | assign(result, { 40 | start: stub, 41 | test: stub, 42 | testEnd: stub, 43 | fail: stub, 44 | success: stub, 45 | comment: stub, 46 | end: stub, 47 | ...reporter.createFormatter?.() || reporter, 48 | }); 49 | 50 | return result; 51 | } 52 | 53 | function run(reporter, type, data) { 54 | if (type === 'start') 55 | return reporter.start(data); 56 | 57 | if (type === 'test') 58 | return reporter.test(data); 59 | 60 | if (type === 'test:end') 61 | return reporter.testEnd(data); 62 | 63 | if (type === 'comment') 64 | return reporter.comment(data); 65 | 66 | if (type === 'success') 67 | return reporter.success(data); 68 | 69 | if (type === 'fail') 70 | return reporter.fail(data); 71 | 72 | if (type === 'end') 73 | return reporter.end(data); 74 | } 75 | -------------------------------------------------------------------------------- /packages/supertape/bin/formatter.spec.mjs: -------------------------------------------------------------------------------- 1 | import {createFormatter} from './formatter.mjs'; 2 | import {test, stub} from '../lib/supertape.mjs'; 3 | 4 | test('supertape: bin: formatter: success', (t) => { 5 | const postMessage = stub(); 6 | const parentPort = { 7 | postMessage, 8 | }; 9 | 10 | const formatter = createFormatter(parentPort); 11 | 12 | const emit = formatter.emit.bind(formatter); 13 | emit('test:success', { 14 | count: 1, 15 | message: 'hello', 16 | }); 17 | 18 | const expected = [ 19 | ['success', { 20 | count: 1, 21 | message: 'hello', 22 | }], 23 | ]; 24 | 25 | t.calledWith(postMessage, expected); 26 | t.end(); 27 | }); 28 | 29 | test('supertape: bin: formatter: comment', (t) => { 30 | const postMessage = stub(); 31 | const parentPort = { 32 | postMessage, 33 | }; 34 | 35 | const formatter = createFormatter(parentPort); 36 | 37 | const emit = formatter.emit.bind(formatter); 38 | emit('comment', { 39 | message: 'hello', 40 | }); 41 | 42 | const expected = [ 43 | ['comment', { 44 | message: 'hello', 45 | }], 46 | ]; 47 | 48 | t.calledWith(postMessage, expected); 49 | t.end(); 50 | }); 51 | 52 | test('supertape: bin: formatter: fail', (t) => { 53 | const postMessage = stub(); 54 | const parentPort = { 55 | postMessage, 56 | }; 57 | 58 | const formatter = createFormatter(parentPort); 59 | 60 | const emit = formatter.emit.bind(formatter); 61 | emit('test:fail', { 62 | count: 1, 63 | result: 1n, 64 | expected: 2, 65 | message: 'hello', 66 | }); 67 | 68 | const expected = [ 69 | ['fail', { 70 | at: undefined, 71 | count: 1, 72 | errorStack: undefined, 73 | expected: '2', 74 | message: 'hello', 75 | operator: undefined, 76 | output: undefined, 77 | result: '1', 78 | }], 79 | ]; 80 | 81 | t.calledWith(postMessage, expected); 82 | t.end(); 83 | }); 84 | -------------------------------------------------------------------------------- /packages/supertape/test/errors.ts: -------------------------------------------------------------------------------- 1 | import test, { 2 | test as superTest, 3 | Test, 4 | stub, 5 | Stub, 6 | extend, 7 | OperationResult, 8 | } from '../lib/supertape.js'; 9 | 10 | // THROWS Expected 2-3 arguments, but got 0 11 | test(); 12 | 13 | test('hello', (t: Test) => { 14 | t.abc(); 15 | t.end(); 16 | }); 17 | 18 | superTest('hello', (t: Test) => { 19 | t.equal(1, 2); 20 | t.end(); 21 | }); 22 | 23 | test('stub should be a function', (t) => { 24 | const fn: Stub = stub(); 25 | fn(); 26 | 27 | t.calledWithNoArgs(fn); 28 | t.end(); 29 | }); 30 | 31 | // THROWS Argument of type 'number' is not assignable to parameter of type 'string'. 32 | test.only(123, (t) => { 33 | t.abc(); 34 | }); 35 | 36 | test.skip('hello', (t: Test) => { 37 | t.end(); 38 | }); 39 | 40 | test.skip('hello', (t: Test) => { 41 | t.end(); 42 | }, {checkAssertionsCount: false}); 43 | 44 | test.only('hello', (t: Test) => { 45 | t.end(); 46 | }, {checkDuplicates: false}); 47 | 48 | test('hello', (t: Test) => { 49 | t.end(); 50 | }, {checkScopes: false}); 51 | 52 | test.only('hello', (t: Test) => { 53 | t.end(); 54 | // THROWS Type 'string' is not assignable to type 'number' 55 | }, {timeout: 'hello'}); 56 | 57 | test('hello', (t: Test) => { 58 | t.end(); 59 | // THROWS Object literal may only specify known properties, and 'checkUnknown' does not exist in type 'TestOptions' 60 | }, {checkUnknown: true}); 61 | 62 | // THROWS Expected 1 arguments, but got 0. 63 | extend(); 64 | 65 | const extendedTest = extend({ 66 | // THROWS Type 'string' is not assignable to type '(operator: Operator) => (...args: any[]) => OperationResult'. 67 | hello: 'world', 68 | 69 | superFail: ({fail}) => (message) => fail(message), 70 | }); 71 | 72 | // THROWS Expected 2-3 arguments, but got 0. 73 | extendedTest(); 74 | extendedTest('hello', (t) => { 75 | t.superFail('world'); 76 | }); 77 | 78 | const minifyExtension = () => async (pass: () => Promise) => { 79 | return await pass(); 80 | }; 81 | 82 | const testAsync = extend({ 83 | minify: minifyExtension, 84 | }); 85 | 86 | testAsync('hello', async (t) => { 87 | await t.minify(); 88 | }); 89 | 90 | -------------------------------------------------------------------------------- /packages/supertape/lib/formatter/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {EventEmitter} = require('node:events'); 4 | const {createHarness} = require('./harness'); 5 | 6 | const resolveFormatter = async (name) => await import(`@supertape/formatter-${name}`); 7 | const isString = (a) => typeof a === 'string'; 8 | 9 | module.exports.createFormatter = async (name) => { 10 | const formatter = new EventEmitter(); 11 | const harness = createHarness(!isString(name) ? name : await resolveFormatter(name)); 12 | 13 | formatter.on('start', ({total}) => { 14 | harness.write({ 15 | type: 'start', 16 | total, 17 | }); 18 | }); 19 | 20 | formatter.on('test', ({test}) => { 21 | harness.write({ 22 | type: 'test', 23 | test, 24 | }); 25 | }); 26 | 27 | formatter.on('test:end', ({count, total, failed, test}) => { 28 | harness.write({ 29 | type: 'test:end', 30 | total, 31 | count, 32 | failed, 33 | test, 34 | }); 35 | }); 36 | 37 | formatter.on('comment', (message) => { 38 | harness.write({ 39 | type: 'comment', 40 | message, 41 | }); 42 | }); 43 | 44 | formatter.on('test:success', ({count, message}) => { 45 | harness.write({ 46 | type: 'success', 47 | count, 48 | message, 49 | }); 50 | }); 51 | 52 | formatter.on('test:fail', ({at, count, message, operator, result, expected, output, errorStack}) => { 53 | harness.write({ 54 | type: 'fail', 55 | at, 56 | count, 57 | message, 58 | operator, 59 | result, 60 | expected, 61 | output, 62 | errorStack, 63 | }); 64 | }); 65 | 66 | formatter.on('end', ({count, passed, failed, skipped}) => { 67 | harness.write({ 68 | type: 'end', 69 | count, 70 | passed, 71 | failed, 72 | skipped, 73 | }); 74 | }); 75 | 76 | return { 77 | formatter, 78 | harness, 79 | }; 80 | }; 81 | -------------------------------------------------------------------------------- /packages/supertape/lib/supertape.d.ts: -------------------------------------------------------------------------------- 1 | import {OperatorStub} from '@supertape/operator-stub'; 2 | import {stub, Stub} from '@cloudcmd/stub'; 3 | 4 | type OperationBaseResult = { 5 | is: boolean; 6 | expected: unknown; 7 | result: unknown; 8 | message: string; 9 | output: string; 10 | }; 11 | type OperationResult = OperationBaseResult | Promise; 12 | type OperatorFn = (...args: any[]) => OperationResult; 13 | type Operator = { 14 | [index: string]: OperatorFn; 15 | }; 16 | type Test = Operator & OperatorStub & { 17 | equal: (result: unknown, expected: unknown, message?: string) => OperationResult; 18 | notEqual: (result: unknown, expected: unknown, message?: string) => OperationResult; 19 | deepEqual: (result: unknown, expected: unknown, message?: string) => OperationResult; 20 | notDeepEqual: (result: unknown, expected: unknown, message?: string) => OperationResult; 21 | fail: (message: string) => OperationResult; 22 | pass: (message: string) => OperationResult; 23 | ok: (result: boolean | unknown, message?: string) => OperationResult; 24 | comment: (message: string) => OperationResult; 25 | notOk: (result: boolean | unknown, message?: string) => OperationResult; 26 | match: (result: string, pattern: string | RegExp, message?: string) => OperationResult; 27 | notMatch: (result: string, pattern: string | RegExp, message?: string) => OperationResult; 28 | end: () => void; 29 | }; 30 | type TestOptions = { 31 | checkAssertionsCount?: boolean; 32 | checkScopes?: boolean; 33 | checkDuplicates?: boolean; 34 | timeout?: number; 35 | }; 36 | 37 | declare function test(message: string, fn: (t: Test) => void, options?: TestOptions): void; 38 | declare const skip: typeof test; 39 | declare const only: typeof test; 40 | 41 | declare namespace test { 42 | export { 43 | only, 44 | skip, 45 | }; 46 | } 47 | 48 | export default test; 49 | 50 | type CustomOperator = { 51 | [index: string]: (operator: Operator) => (...args: any[]) => OperationResult; 52 | }; 53 | 54 | declare function extend(customOperator: CustomOperator): typeof test; 55 | export { 56 | test, 57 | Test, 58 | stub, 59 | Stub, 60 | extend, 61 | OperationResult, 62 | }; 63 | -------------------------------------------------------------------------------- /packages/supertape/lib/formatter/harness.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {once} = require('node:events'); 4 | const {Transform} = require('node:stream'); 5 | const {stub} = require('supertape'); 6 | 7 | const test = require('../..'); 8 | const {createHarness} = require('./harness'); 9 | const {keys} = Object; 10 | 11 | test('supertape: harness: proceed condition', (t) => { 12 | const reporter = { 13 | test: () => '', 14 | }; 15 | 16 | let length = 1; 17 | const input = createHarness(reporter); 18 | 19 | const output = new Transform({ 20 | transform(chunk, enc, callback) { 21 | ({length} = chunk); 22 | callback(); 23 | }, 24 | }); 25 | 26 | input.pipe(output); 27 | 28 | input.write({ 29 | type: 'test', 30 | }); 31 | 32 | t.ok(length, 'should proceed only when reporter return not zero length chunk'); 33 | t.end(); 34 | }); 35 | 36 | test('supertape: harness: proceed condition: write after end', async (t) => { 37 | const reporter = { 38 | test: () => '', 39 | }; 40 | 41 | const input = createHarness(reporter); 42 | const output = new Transform({ 43 | transform(chunk, enc, callback) { 44 | callback(); 45 | }, 46 | }); 47 | 48 | input.pipe(output); 49 | 50 | input.write({ 51 | type: 'end', 52 | }); 53 | 54 | const [[error]] = await Promise.all([ 55 | once(input, 'error'), 56 | input.write({ 57 | type: 'test', 58 | }), 59 | ]); 60 | 61 | t.equal(error.message, `☝️ Looks like 'async' operator called without 'await'`); 62 | t.end(); 63 | }); 64 | 65 | test('supertape: harness: no readableObjectMode, since it breaks console.log', (t) => { 66 | const reporter = { 67 | test: () => '', 68 | }; 69 | 70 | const Transform = stub(); 71 | 72 | createHarness(reporter, { 73 | Transform, 74 | }); 75 | 76 | const {args} = Transform; 77 | const [[arg]] = args; 78 | const result = keys(arg); 79 | 80 | const expected = [ 81 | 'writableObjectMode', 82 | 'transform', 83 | ]; 84 | 85 | t.deepEqual(result, expected); 86 | t.end(); 87 | }); 88 | -------------------------------------------------------------------------------- /packages/formatter-fail/lib/fail.spec.js: -------------------------------------------------------------------------------- 1 | import montag from 'montag'; 2 | import pullout from 'pullout'; 3 | import {test, createTest} from 'supertape'; 4 | import * as failFormatter from './fail.js'; 5 | 6 | const pull = async (stream, i = 9) => { 7 | const output = await pullout(await stream); 8 | 9 | return output 10 | .split('\n') 11 | .slice(0, i) 12 | .join('\n'); 13 | }; 14 | 15 | test('supertape: format: fail', async (t) => { 16 | const successFn = (t) => { 17 | t.ok(true); 18 | t.end(); 19 | }; 20 | 21 | const successMessage = 'format: success'; 22 | 23 | const failFn = (t) => { 24 | t.ok(false); 25 | t.end(); 26 | }; 27 | 28 | const failMessage = 'format: fail'; 29 | 30 | const { 31 | test, 32 | stream, 33 | run, 34 | } = await createTest({ 35 | format: failFormatter, 36 | }); 37 | 38 | test(successMessage, successFn); 39 | test(failMessage, failFn); 40 | 41 | const [result] = await Promise.all([ 42 | pull(stream), 43 | run(), 44 | ]); 45 | 46 | const expected = montag` 47 | TAP version 13 48 | # format: fail 49 | not ok 2 should be truthy 50 | --- 51 | operator: ok 52 | expected: |- 53 | true 54 | result: |- 55 | false 56 | `; 57 | 58 | t.equal(result, expected); 59 | t.end(); 60 | }); 61 | 62 | test('supertape: format: fail: skip', async (t) => { 63 | const fn = (t) => { 64 | t.ok(true); 65 | t.end(); 66 | }; 67 | 68 | const message = 'skip: success'; 69 | 70 | const { 71 | test, 72 | stream, 73 | run, 74 | } = await createTest({ 75 | format: failFormatter, 76 | }); 77 | 78 | test(message, fn, { 79 | skip: true, 80 | }); 81 | 82 | const [result] = await Promise.all([ 83 | pull(stream, 8), 84 | run(), 85 | ]); 86 | 87 | const expected = montag` 88 | TAP version 13 89 | 90 | 1..0 91 | # tests 0 92 | # pass 0 93 | # skip 1 94 | 95 | # ok 96 | `; 97 | 98 | t.equal(result, expected); 99 | t.end(); 100 | }); 101 | -------------------------------------------------------------------------------- /packages/supertape/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "supertape", 3 | "version": "11.4.0", 4 | "author": "coderaiser (https://github.com/coderaiser)", 5 | "description": "📼 Supertape simplest high speed test runner with superpowers", 6 | "homepage": "http://github.com/coderaiser/supertape", 7 | "main": "./lib/supertape.js", 8 | "exports": { 9 | ".": { 10 | "node": { 11 | "require": "./lib/supertape.js", 12 | "import": "./lib/supertape.mjs" 13 | }, 14 | "default": "./lib/supertape.js" 15 | }, 16 | "./bin/supertape": "./bin/supertape.mjs", 17 | "./cli": "./lib/cli.js", 18 | "./exit-codes": "./lib/exit-codes.js" 19 | }, 20 | "type": "commonjs", 21 | "bin": { 22 | "tape": "bin/tracer.mjs", 23 | "supertape": "bin/tracer.mjs" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/coderaiser/supertape.git" 28 | }, 29 | "scripts": { 30 | "test": "madrun test", 31 | "test:dts": "madrun test:dts", 32 | "watch:test": "madrun watch:test", 33 | "lint": "madrun lint", 34 | "fresh:lint": "madrun fresh:lint", 35 | "lint:fresh": "madrun lint:fresh", 36 | "fix:lint": "madrun fix:lint", 37 | "coverage": "madrun coverage", 38 | "report": "madrun report", 39 | "wisdom": "madrun wisdom" 40 | }, 41 | "dependencies": { 42 | "@cloudcmd/stub": "^4.0.0", 43 | "@putout/cli-keypress": "^3.0.0", 44 | "@putout/cli-validate-args": "^2.0.0", 45 | "@supertape/engine-loader": "^2.0.0", 46 | "@supertape/formatter-fail": "^4.0.0", 47 | "@supertape/formatter-json-lines": "^2.0.0", 48 | "@supertape/formatter-progress-bar": "^7.0.0", 49 | "@supertape/formatter-short": "^3.0.0", 50 | "@supertape/formatter-tap": "^4.0.0", 51 | "@supertape/formatter-time": "^2.0.0", 52 | "@supertape/operator-stub": "^3.0.0", 53 | "cli-progress": "^3.8.2", 54 | "flatted": "^3.3.1", 55 | "fullstore": "^3.0.0", 56 | "glob": "^11.0.1", 57 | "jest-diff": "^30.0.3", 58 | "json-with-bigint": "^3.4.4", 59 | "once": "^1.4.0", 60 | "resolve": "^1.17.0", 61 | "stacktracey": "^2.1.7", 62 | "try-to-catch": "^3.0.0", 63 | "wraptile": "^3.0.0", 64 | "yargs-parser": "^22.0.0" 65 | }, 66 | "keywords": [ 67 | "function", 68 | "promise", 69 | "async", 70 | "await", 71 | "then", 72 | "tap", 73 | "tape", 74 | "testing" 75 | ], 76 | "devDependencies": { 77 | "@iocmd/wait": "^2.1.0", 78 | "@putout/eslint-flat": "^3.0.0", 79 | "c8": "^10.1.2", 80 | "check-dts": "^0.9.0", 81 | "currify": "^4.0.0", 82 | "eslint": "^9.1.1", 83 | "eslint-plugin-putout": "^29.0.2", 84 | "find-up": "^8.0.0", 85 | "madrun": "^11.0.0", 86 | "mock-require": "^3.0.2", 87 | "montag": "^1.0.0", 88 | "nodemon": "^3.0.1", 89 | "pullout": "^5.0.1", 90 | "putout": "^41.0.2", 91 | "runsome": "^1.0.0", 92 | "try-catch": "^3.0.1", 93 | "typescript": "^5.1.6" 94 | }, 95 | "license": "MIT", 96 | "engines": { 97 | "node": ">=20" 98 | }, 99 | "publishConfig": { 100 | "access": "public" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /packages/formatter-json-lines/lib/json-lines.spec.js: -------------------------------------------------------------------------------- 1 | import montag from 'montag'; 2 | import pullout from 'pullout'; 3 | import {test, createTest} from 'supertape'; 4 | 5 | const {parse} = JSON; 6 | 7 | const pull = async (stream, i = 9) => { 8 | const output = await pullout(await stream); 9 | 10 | return output 11 | .split('\n') 12 | .slice(0, i) 13 | .join('\n'); 14 | }; 15 | 16 | test('supertape: format: json-lines', async (t) => { 17 | const successFn = (t) => { 18 | t.ok(true); 19 | t.end(); 20 | }; 21 | 22 | const successMessage = 'json-lines: success'; 23 | 24 | const tapFn = (t) => { 25 | t.ok(false); 26 | t.end(); 27 | }; 28 | 29 | const tapMessage = 'json-lines: tap'; 30 | 31 | const { 32 | test, 33 | run, 34 | stream, 35 | } = await createTest({ 36 | format: 'json-lines', 37 | }); 38 | 39 | test(successMessage, successFn); 40 | test(tapMessage, tapFn); 41 | 42 | const [result] = await Promise.all([ 43 | pull(stream, 2), 44 | run(), 45 | ]); 46 | 47 | const expected = montag` 48 | {"count":1,"total":2,"failed":0,"test":"json-lines: success"} 49 | {"count":2,"total":2,"failed":1,"test":"json-lines: tap"} 50 | `; 51 | 52 | t.equal(result, expected); 53 | t.end(); 54 | }); 55 | 56 | test('supertape: format: json-lines: skip', async (t) => { 57 | const fn = (t) => { 58 | t.ok(true); 59 | t.end(); 60 | }; 61 | 62 | const message = 'success'; 63 | 64 | const { 65 | test, 66 | run, 67 | stream, 68 | } = await createTest({ 69 | format: 'json-lines', 70 | }); 71 | 72 | test(message, fn, { 73 | skip: true, 74 | }); 75 | 76 | const [result] = await Promise.all([ 77 | pull(stream, 8), 78 | run(), 79 | ]); 80 | 81 | const parsed = parse(result); 82 | 83 | const expected = { 84 | count: 0, 85 | passed: 0, 86 | failed: 0, 87 | skipped: 1, 88 | }; 89 | 90 | t.deepEqual(parsed, expected); 91 | t.end(); 92 | }); 93 | 94 | test('supertape: format: json-lines: comment', async (t) => { 95 | const successFn = (t) => { 96 | t.ok(true); 97 | t.end(); 98 | }; 99 | 100 | const successMessage = 'json-lines: success'; 101 | 102 | const tapFn = (t) => { 103 | t.comment('hello'); 104 | t.ok(false); 105 | t.end(); 106 | }; 107 | 108 | const tapMessage = 'json-lines: tap'; 109 | 110 | const { 111 | test, 112 | run, 113 | stream, 114 | } = await createTest({ 115 | format: 'json-lines', 116 | }); 117 | 118 | test(successMessage, successFn); 119 | test(tapMessage, tapFn); 120 | 121 | const [result] = await Promise.all([ 122 | pull(stream, 12), 123 | run(), 124 | ]); 125 | 126 | const parsed = result 127 | .split('\n') 128 | .filter(Boolean) 129 | .map(parse); 130 | 131 | const FAIL = 2; 132 | 133 | delete parsed[FAIL].at; 134 | delete parsed[FAIL].errorStack; 135 | 136 | const expected = [{ 137 | count: 1, 138 | failed: 0, 139 | test: 'json-lines: success', 140 | total: 2, 141 | }, { 142 | count: 2, 143 | failed: 1, 144 | test: 'json-lines: tap', 145 | total: 2, 146 | }, { 147 | result: false, 148 | count: 2, 149 | expected: true, 150 | message: 'should be truthy', 151 | operator: 'ok', 152 | test: 'json-lines: tap', 153 | }, { 154 | count: 2, 155 | failed: 1, 156 | passed: 1, 157 | skipped: 0, 158 | }]; 159 | 160 | t.deepEqual(parsed, expected); 161 | t.end(); 162 | }); 163 | -------------------------------------------------------------------------------- /packages/supertape/lib/validator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const once = require('once'); 4 | const StackTracey = require('stacktracey'); 5 | const getDuplicatesMessage = ([, a]) => a; 6 | 7 | const getMessage = ({message, at, validations}) => [ 8 | message, 9 | at, 10 | validations, 11 | ]; 12 | 13 | const getMessagesList = (tests) => tests.map(getMessage); 14 | const compareMessage = (a) => ([b]) => a === b; 15 | const SCOPE_DEFINED = /^.*[\w-/\d\s]+:.*/; 16 | const processedList = new Set(); 17 | 18 | const validations = { 19 | checkDuplicates: true, 20 | checkScopes: false, 21 | checkAssertionsCount: true, 22 | }; 23 | 24 | const validators = { 25 | checkDuplicates, 26 | checkScopes, 27 | checkAssertionsCount, 28 | }; 29 | 30 | const { 31 | assign, 32 | values, 33 | entries, 34 | } = Object; 35 | 36 | const findByMessage = (msg, tests) => { 37 | const getMessages = once(getMessagesList); 38 | 39 | return getMessages(tests).filter(compareMessage(msg)); 40 | }; 41 | 42 | module.exports.setValidations = ({checkDuplicates, checkScopes, checkAssertionsCount}) => { 43 | assign(validations, { 44 | checkDuplicates, 45 | checkScopes, 46 | checkAssertionsCount, 47 | }); 48 | }; 49 | 50 | const isValidationEnabled = (a) => values(a).filter(Boolean).length; 51 | 52 | module.exports.createValidator = ({tests}) => (msg, options) => { 53 | if (!isValidationEnabled(validations)) 54 | return []; 55 | 56 | for (const [name, enabled] of entries(validations)) { 57 | if (!enabled) 58 | continue; 59 | 60 | const filtered = findByMessage(msg, tests); 61 | 62 | if (!filtered.length) 63 | throw Error('☝️Looks like message cannot be find in tests, this should never happen'); 64 | 65 | const [message, at] = validators[name](msg, filtered, options); 66 | 67 | if (at) 68 | return [message, at]; 69 | } 70 | 71 | return []; 72 | }; 73 | 74 | module.exports.getAt = () => getFileName(); 75 | 76 | const CALLS_FROM_TEST = 3; 77 | 78 | function getFileName() { 79 | const {items} = new StackTracey(Error()); 80 | 81 | for (const {beforeParse, file} of items.slice(CALLS_FROM_TEST)) { 82 | if (file.includes('node_modules')) 83 | continue; 84 | 85 | return beforeParse; 86 | } 87 | 88 | return ''; 89 | } 90 | 91 | function checkAssertionsCount(msg, filtered, options) { 92 | const {assertionsCount} = options; 93 | const [, at] = filtered[0]; 94 | 95 | if (!isEnabled(filtered, 'checkAssertionsCount')) 96 | return []; 97 | 98 | if (assertionsCount > 1) 99 | return [ 100 | `Only one assertion per test allowed, looks like you have more`, 101 | at, 102 | ]; 103 | 104 | if (!assertionsCount) 105 | return [ 106 | `Only one assertion per test allowed, looks like you have none`, 107 | at, 108 | ]; 109 | 110 | return []; 111 | } 112 | 113 | function checkScopes(msg, filtered) { 114 | const [message, at] = filtered[0]; 115 | 116 | if (!SCOPE_DEFINED.test(message)) 117 | return [ 118 | `Scope should be defined before first colon: 'scope: subject', received: '${message}'`, 119 | at, 120 | ]; 121 | 122 | return []; 123 | } 124 | 125 | const isEnabled = (tests, name) => { 126 | for (const [, , validations] of tests) { 127 | if (!validations[name]) 128 | return false; 129 | } 130 | 131 | return true; 132 | }; 133 | 134 | function checkDuplicates(msg, filtered) { 135 | if (filtered.length < 2) 136 | return []; 137 | 138 | if (!isEnabled(filtered, 'checkDuplicates')) 139 | return []; 140 | 141 | const [first, second] = filtered.map(getDuplicatesMessage); 142 | 143 | if (processedList.has(first)) 144 | return []; 145 | 146 | processedList.add(first); 147 | return [ 148 | `Duplicate ${first}`, 149 | second, 150 | ]; 151 | } 152 | -------------------------------------------------------------------------------- /packages/supertape/lib/format.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tryCatch = require('try-catch'); 4 | const montag = require('montag'); 5 | 6 | const test = require('./supertape'); 7 | const {parseAt} = require('./format'); 8 | 9 | test('supertape: format', (t) => { 10 | const stack = `Error: ENOENT: no such file or directory, open '/abc'`; 11 | const result = parseAt(stack, { 12 | reason: 'user', 13 | }); 14 | 15 | t.equal(result, stack); 16 | t.end(); 17 | }); 18 | 19 | test('supertape: format: reason: exception', (t) => { 20 | const stack = ` 21 | at file:///Users/coderaiser/estrace/lib/estrace.spec.js:57:11 22 | at async module.exports (/Users/coderaiser/estrace/node_modules/try-to-catch/lib/try-to-catch.js:7:23) 23 | at async runOneTest (/Users/coderaiser/estrace/node_modules/supertape/lib/run-tests.js:143:21) 24 | at async runTests (/Users/coderaiser/estrace/node_modules/supertape/lib/run-tests.js:92:9) 25 | at async module.exports (/Users/coderaiser/estrace/node_modules/supertape/lib/run-tests.js:55:12) 26 | at async EventEmitter. (/Users/coderaiser/estrace/node_modules/supertape/lib/supertape.js:69:26) 27 | `; 28 | 29 | const result = parseAt(stack, { 30 | reason: 'exception', 31 | }); 32 | 33 | const expected = 'at file:///Users/coderaiser/estrace/lib/estrace.spec.js:57:11'; 34 | 35 | t.equal(result, expected); 36 | t.end(); 37 | }); 38 | 39 | test('supertape: format: mock-import: ', (t) => { 40 | const stack = ` 41 | at file:///Users/coderaiser/estrace/lib/estrace.spec.js?mock-import-count=55:57:11 42 | at async module.exports (/Users/coderaiser/estrace/node_modules/try-to-catch/lib/try-to-catch.js:7:23) 43 | at async runOneTest (/Users/coderaiser/estrace/node_modules/supertape/lib/run-tests.js:143:21) 44 | at async runTests (/Users/coderaiser/estrace/node_modules/supertape/lib/run-tests.js:92:9) 45 | at async module.exports (/Users/coderaiser/estrace/node_modules/supertape/lib/run-tests.js:55:12) 46 | at async EventEmitter. (/Users/coderaiser/estrace/node_modules/supertape/lib/supertape.js:69:26) 47 | `; 48 | 49 | const result = parseAt(stack, { 50 | reason: 'exception', 51 | }); 52 | 53 | const expected = 'at file:///Users/coderaiser/estrace/lib/estrace.spec.js:57:11'; 54 | 55 | t.equal(result, expected); 56 | t.end(); 57 | }); 58 | 59 | test('supertape: format: parseAt: less then 4', (t) => { 60 | const stack = montag` 61 | Error: should deep equal 62 | at run (file:///Users/coderaiser/putout/node_modules/supertape/lib/operators.mjs:275:33) 63 | at validateEnd.name.name (file:///Users/coderaiser/putout/node_modules/supertape/lib/operators.mjs:190:13)' 64 | `; 65 | 66 | const [error] = tryCatch(parseAt, stack, { 67 | reason: 'user', 68 | }); 69 | 70 | const expected = 'Error: should deep equal'; 71 | 72 | t.match(error.message, expected); 73 | t.end(); 74 | }); 75 | 76 | test('supertape: format: parseAt: looks like empty', (t) => { 77 | const stack = montag` 78 | Error: ☝️Looks like provided fixture cannot be parsed: 'const foo1 = 79 | // comment 80 | () => () => 1; 81 | 82 | const foo1 = 83 | // sdf 84 | 5' 85 | at run (operators.mjs:272:33) 86 | at Object.print (operators.mjs:207:9) 87 | at comment.spec.js:322:7 88 | at module.exports (try-to-catch.js:7:29) 89 | at runOneTest (run-tests.js:165:45) 90 | at async runTests (run-tests.js:83:9) 91 | at async module.exports (run-tests.js:37:16) 92 | at async EventEmitter. (supertape.js:85:24) 93 | `; 94 | 95 | const result = parseAt(stack, { 96 | reason: 'user', 97 | }); 98 | 99 | const expected = `Error: ☝️Looks like provided fixture cannot be parsed: 'const foo1 =`; 100 | 101 | t.match(result, expected); 102 | t.end(); 103 | }); 104 | -------------------------------------------------------------------------------- /packages/operator-stub/README.md: -------------------------------------------------------------------------------- 1 | # @supertape/operator-stub [![NPM version][NPMIMGURL]][NPMURL] [![Build Status][BuildStatusIMGURL]][BuildStatusURL] [![Coverage Status][CoverageIMGURL]][CoverageURL] 2 | 3 | [NPMIMGURL]: https://img.shields.io/npm/v/supertape.svg?style=flat&longCache=true 4 | [BuildStatusIMGURL]: https://img.shields.io/travis/coderaiser/supertape/master.svg?style=flat&longCache=true 5 | [NPMURL]: https://npmjs.org/package/supertape "npm" 6 | [BuildStatusURL]: https://travis-ci.org/coderaiser/supertape "Build Status" 7 | [CoverageURL]: https://coveralls.io/github/coderaiser/supertape?branch=master 8 | [CoverageIMGURL]: https://coveralls.io/repos/coderaiser/supertape/badge.svg?branch=master&service=github 9 | 10 | `supertape` operator simplifies work with [@cloudcmd/stub](https://github.com/cloudcmd/stub). 11 | 12 | ## Install 13 | 14 | ``` 15 | npm i @supertape/operator-stub -D 16 | ``` 17 | 18 | ## Operators 19 | 20 | Adds next operators to work with: 21 | 22 | ### t.calledWith(fn, args [, message]) 23 | 24 | ```js 25 | import test, {stub} from 'supertape'; 26 | 27 | test('function call', (t) => { 28 | const fn = stub(); 29 | 30 | fn('hello', 'world'); 31 | 32 | t.calledWith(fn, ['hello', 'world'], 'fn should be called with "hello", "world"'); 33 | t.end(); 34 | }); 35 | ``` 36 | 37 | ### t.calledWithNoArgs(fn[, message]) 38 | 39 | ```js 40 | import test, {stub} from 'supertape'; 41 | 42 | test('function called with no args', (t) => { 43 | const fn = stub(); 44 | 45 | fn(); 46 | 47 | t.calledWithNoArgs(fn); 48 | t.end(); 49 | }); 50 | ``` 51 | 52 | ### t.calledCount(fn, count[, message]) 53 | 54 | ```js 55 | import test, {stub} from 'supertape'; 56 | 57 | test('function called count', (t) => { 58 | const fn = stub(); 59 | 60 | fn(); 61 | fn(); 62 | 63 | t.calledCount(fn, 2); 64 | t.end(); 65 | }); 66 | ``` 67 | 68 | ### t.calledOnce(fn [, message]) 69 | 70 | ```js 71 | import test, {stub} from 'supertape'; 72 | 73 | test('function called once', (t) => { 74 | const fn = stub(); 75 | 76 | fn('hello'); 77 | 78 | t.calledOnce(fn); 79 | t.end(); 80 | }); 81 | ``` 82 | 83 | ### t.calledTwice(fn, count[, message]) 84 | 85 | ```js 86 | import test, {stub} from 'supertape'; 87 | 88 | test('function called twice', (t) => { 89 | const fn = stub(); 90 | 91 | fn('hello'); 92 | fn('world'); 93 | 94 | t.calledTwice(fn); 95 | t.end(); 96 | }); 97 | ``` 98 | 99 | ### t.calledWithNew(fn, count[, message]) 100 | 101 | ```js 102 | import test, {stub} from 'supertape'; 103 | 104 | test('function called with new', (t) => { 105 | const fn = stub(); 106 | 107 | new fn(); 108 | 109 | t.calledWithNew(fn); 110 | t.end(); 111 | }); 112 | ``` 113 | 114 | ### t.calledBefore(fn1, fn2[, message]) 115 | 116 | Check that `fn1` called before `fn2`. 117 | Do not forget to set names of stubs. 118 | 119 | ```js 120 | import test, {stub} from 'supertape'; 121 | 122 | test('function called with new', (t) => { 123 | const init = stub().withName('init'); 124 | const show = stub().withName('show'); 125 | 126 | init(); 127 | show(); 128 | 129 | t.calledBefore(show, init); 130 | t.end(); 131 | }); 132 | ``` 133 | 134 | ### t.calledAfter(fn1, fn2[, message]) 135 | 136 | Check that `fn1` called after `fn2`. 137 | Do not forget to set names of stubs. 138 | 139 | ```js 140 | import test, {stub} from 'supertape'; 141 | 142 | test('function called with new', (t) => { 143 | const init = stub().withName('init'); 144 | const show = stub().withName('show'); 145 | 146 | init(); 147 | show(); 148 | 149 | t.calledAfter(init, show); 150 | t.end(); 151 | }); 152 | ``` 153 | 154 | ### t.calledInOrder([fn1, fn2, fnN][, message]) 155 | 156 | Check that array of stubs `fns` called in order; 157 | Do not forget to set names of stubs. 158 | 159 | ```js 160 | import test, {stub} from 'supertape'; 161 | 162 | test('function called with new', (t) => { 163 | const init = stub().withName('init'); 164 | const show = stub().withName('show'); 165 | 166 | init(); 167 | show(); 168 | 169 | t.calledInOrder([init, show]); 170 | t.end(); 171 | }); 172 | ``` 173 | 174 | ## License 175 | 176 | MIT 177 | -------------------------------------------------------------------------------- /packages/supertape/lib/cli.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const process = require('node:process'); 4 | const {resolve: resolvePath} = require('node:path'); 5 | const {once} = require('node:events'); 6 | const {pathToFileURL} = require('node:url'); 7 | 8 | const glob = require('glob'); 9 | const fullstore = require('fullstore'); 10 | const tryToCatch = require('try-to-catch'); 11 | const {keypress: _keypress} = require('@putout/cli-keypress'); 12 | 13 | const {parseArgs, yargsOptions} = require('./cli/parse-args'); 14 | 15 | const _supertape = require('..'); 16 | const { 17 | OK, 18 | FAIL, 19 | WAS_STOP, 20 | UNHANDLED, 21 | INVALID_OPTION, 22 | SKIPPED, 23 | } = require('./exit-codes'); 24 | 25 | const isExclude = (a) => !a.includes('node_modules'); 26 | const removeDuplicates = (a) => Array.from(new Set(a)); 27 | 28 | const filesCount = fullstore(0); 29 | 30 | const { 31 | SUPERTAPE_CHECK_SKIPPED = '0', 32 | } = process.env; 33 | 34 | module.exports = async (overrides = {}) => { 35 | const { 36 | argv, 37 | cwd, 38 | stdout, 39 | stderr, 40 | exit, 41 | workerFormatter, 42 | keypress = _keypress, 43 | supertape = _supertape, 44 | } = overrides; 45 | 46 | const isStop = overrides.isStop || keypress().isStop; 47 | 48 | const [error, result] = await tryToCatch(cli, { 49 | supertape, 50 | argv, 51 | cwd, 52 | stdout, 53 | exit, 54 | isStop, 55 | workerFormatter, 56 | }); 57 | 58 | if (error) { 59 | stderr.write(error.stack); 60 | return exit(UNHANDLED); 61 | } 62 | 63 | const { 64 | failed, 65 | code, 66 | message, 67 | skipped, 68 | } = result; 69 | 70 | if (isStop()) 71 | return exit(WAS_STOP); 72 | 73 | if (Number(SUPERTAPE_CHECK_SKIPPED) && skipped) 74 | return exit(SKIPPED); 75 | 76 | if (failed) 77 | return exit(FAIL); 78 | 79 | if (code === INVALID_OPTION) { 80 | stderr.write(`${message}\n`); 81 | return exit(code); 82 | } 83 | 84 | return exit(OK); 85 | }; 86 | 87 | async function cli({argv, cwd, stdout, isStop, workerFormatter, supertape}) { 88 | const args = parseArgs(argv); 89 | 90 | if (args.version) { 91 | stdout.write(`v${require('../package').version}\n`); 92 | return OK; 93 | } 94 | 95 | if (args.help) { 96 | const {help} = await import('./help.js'); 97 | stdout.write(help()); 98 | 99 | return OK; 100 | } 101 | 102 | const {validateArgs} = await import('@putout/cli-validate-args'); 103 | 104 | const error = await validateArgs(args, [ 105 | ...yargsOptions.boolean, 106 | ...yargsOptions.string, 107 | ]); 108 | 109 | if (error) 110 | return { 111 | code: INVALID_OPTION, 112 | message: error.message, 113 | }; 114 | 115 | for (const module of args.require) 116 | await import(module); 117 | 118 | const allFiles = []; 119 | 120 | for (const arg of args._) { 121 | const files = glob 122 | .sync(arg) 123 | .filter(isExclude); 124 | 125 | allFiles.push(...files); 126 | } 127 | 128 | const { 129 | format, 130 | checkDuplicates, 131 | checkScopes, 132 | checkAssertionsCount, 133 | } = args; 134 | 135 | supertape.init({ 136 | run: false, 137 | quiet: true, 138 | format, 139 | isStop, 140 | checkDuplicates, 141 | checkScopes, 142 | checkAssertionsCount, 143 | workerFormatter, 144 | }); 145 | 146 | const stream = await supertape.createStream(); 147 | 148 | stream.pipe(stdout); 149 | 150 | const files = removeDuplicates(allFiles); 151 | 152 | if (!files.length) 153 | return OK; 154 | 155 | const resolvedNames = []; 156 | 157 | // always resolve before import for windows 158 | for (const file of files) { 159 | resolvedNames.push(pathToFileURL(resolvePath(cwd, file))); 160 | } 161 | 162 | filesCount(files.length); 163 | 164 | for (const resolved of resolvedNames) 165 | await import(resolved); 166 | 167 | const [result] = await once(supertape.run(), 'end'); 168 | 169 | return result; 170 | } 171 | 172 | module.exports._filesCount = filesCount; 173 | -------------------------------------------------------------------------------- /packages/formatter-tap/lib/tap.spec.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import montag from 'montag'; 3 | import pullout from 'pullout'; 4 | import {test, createTest} from 'supertape'; 5 | import * as tapFormatter from './tap.js'; 6 | 7 | const pull = async (stream, i = 9) => { 8 | const output = await pullout(await stream); 9 | 10 | return output 11 | .split('\n') 12 | .slice(0, i) 13 | .join('\n'); 14 | }; 15 | 16 | test('supertape: format: tap', async (t) => { 17 | const successFn = (t) => { 18 | t.ok(true); 19 | t.end(); 20 | }; 21 | 22 | const successMessage = 'format: success'; 23 | 24 | const tapFn = (t) => { 25 | t.ok(false); 26 | t.end(); 27 | }; 28 | 29 | const tapMessage = 'format: tap'; 30 | 31 | const { 32 | test, 33 | stream, 34 | run, 35 | } = await createTest({ 36 | formatter: tapFormatter, 37 | }); 38 | 39 | test(successMessage, successFn); 40 | test(tapMessage, tapFn); 41 | 42 | const [result] = await Promise.all([ 43 | pull(stream, 11), 44 | run(), 45 | ]); 46 | 47 | const expected = montag` 48 | TAP version 13 49 | # format: success 50 | ok 1 should be truthy 51 | # format: tap 52 | not ok 2 should be truthy 53 | --- 54 | operator: ok 55 | expected: |- 56 | true 57 | result: |- 58 | false 59 | `; 60 | 61 | t.equal(result, expected); 62 | t.end(); 63 | }); 64 | 65 | test('supertape: format: tap: skip', async (t) => { 66 | const fn = (t) => { 67 | t.ok(true); 68 | t.end(); 69 | }; 70 | 71 | const message = 'success'; 72 | const { 73 | test, 74 | stream, 75 | run, 76 | } = await createTest({ 77 | formatter: tapFormatter, 78 | }); 79 | 80 | test(message, fn, { 81 | skip: true, 82 | }); 83 | 84 | const [result] = await Promise.all([ 85 | pull(stream, 8), 86 | run(), 87 | ]); 88 | 89 | delete process.env.SUPERTAPE_NO_PROGRESS_BAR; 90 | 91 | const expected = montag` 92 | TAP version 13 93 | 94 | 1..0 95 | # tests 0 96 | # pass 0 97 | # skip 1 98 | 99 | # ok 100 | `; 101 | 102 | t.equal(result, expected); 103 | t.end(); 104 | }); 105 | 106 | test('supertape: format: tap: comment', async (t) => { 107 | const successFn = (t) => { 108 | t.ok(true); 109 | t.end(); 110 | }; 111 | 112 | const successMessage = 'format: success'; 113 | 114 | const tapFn = (t) => { 115 | t.comment('hello'); 116 | t.ok(false); 117 | t.end(); 118 | }; 119 | 120 | const tapMessage = 'format: tap'; 121 | 122 | const { 123 | test, 124 | stream, 125 | run, 126 | } = await createTest({ 127 | formatter: tapFormatter, 128 | }); 129 | 130 | test(successMessage, successFn); 131 | test(tapMessage, tapFn); 132 | 133 | const [result] = await Promise.all([ 134 | pull(stream, 12), 135 | run(), 136 | ]); 137 | 138 | const expected = montag` 139 | TAP version 13 140 | # format: success 141 | ok 1 should be truthy 142 | # format: tap 143 | # hello 144 | not ok 2 should be truthy 145 | --- 146 | operator: ok 147 | expected: |- 148 | true 149 | result: |- 150 | false 151 | `; 152 | 153 | t.equal(result, expected); 154 | t.end(); 155 | }); 156 | 157 | test('supertape: formatter: tap: output', async (t) => { 158 | const fn = (t) => { 159 | t.equal('hello', 'world'); 160 | t.end(); 161 | }; 162 | 163 | const message = 'hello world'; 164 | 165 | const { 166 | test, 167 | stream, 168 | run, 169 | } = await createTest({ 170 | formatter: tapFormatter, 171 | }); 172 | 173 | test(message, fn); 174 | 175 | const BEFORE_DIFF = 6; 176 | const [result] = await Promise.all([ 177 | pull(stream, BEFORE_DIFF), 178 | run(), 179 | ]); 180 | 181 | const expected = montag` 182 | TAP version 13 183 | # hello world 184 | not ok 1 should equal 185 | --- 186 | operator: equal 187 | diff: |- 188 | `; 189 | 190 | t.equal(result, expected); 191 | t.end(); 192 | }); 193 | -------------------------------------------------------------------------------- /packages/supertape/lib/run-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fullstore = require('fullstore'); 4 | const wraptile = require('wraptile'); 5 | const tryToCatch = require('try-to-catch'); 6 | const isDebug = require('./is-debug'); 7 | 8 | const {createValidator} = require('./validator'); 9 | 10 | const inc = wraptile((store) => store(store() + 1)); 11 | const isOnly = ({only}) => only; 12 | const isSkip = ({skip}) => skip; 13 | const notSkip = ({skip}) => !skip; 14 | const parseTime = (a) => a === 'undefined' ? 1 : a; 15 | 16 | const getInitOperators = async () => await import('./operators.mjs'); 17 | 18 | const DEBUG_TIME = 3000 * 1000; 19 | 20 | const doTimeout = (time, value) => { 21 | let stop; 22 | 23 | if (isDebug) 24 | time = DEBUG_TIME; 25 | 26 | const promise = new Promise((resolve) => { 27 | const id = setTimeout(resolve, parseTime(time), value); 28 | stop = clearTimeout.bind(null, id); 29 | }); 30 | 31 | return [promise, stop]; 32 | }; 33 | 34 | module.exports = async (tests, {formatter, operators, isStop}) => { 35 | const onlyTests = tests.filter(isOnly); 36 | 37 | if (onlyTests.length) 38 | return await runTests(onlyTests, { 39 | formatter, 40 | operators, 41 | skipped: tests.length - onlyTests.length, 42 | isStop, 43 | }); 44 | 45 | const notSkippedTests = tests.filter(notSkip); 46 | const skipped = tests.filter(isSkip).length; 47 | 48 | return await runTests(notSkippedTests, { 49 | formatter, 50 | operators, 51 | skipped, 52 | isStop, 53 | }); 54 | }; 55 | 56 | async function runTests(tests, {formatter, operators, skipped, isStop}) { 57 | const count = fullstore(0); 58 | const failed = fullstore(0); 59 | const passed = fullstore(0); 60 | const incCount = inc(count); 61 | const incFailed = inc(failed); 62 | const incPassed = inc(passed); 63 | const total = tests.length; 64 | 65 | formatter.emit('start', { 66 | total, 67 | }); 68 | 69 | const wasStop = fullstore(); 70 | 71 | const getValidationMessage = createValidator({ 72 | tests, 73 | }); 74 | 75 | for (const {fn, message, timeout, extensions, at, validations} of tests) { 76 | if (wasStop()) 77 | break; 78 | 79 | if (isStop()) { 80 | wasStop(true); 81 | count(total - 1); 82 | } 83 | 84 | await runOneTest({ 85 | at, 86 | fn, 87 | message, 88 | formatter, 89 | count, 90 | total, 91 | failed, 92 | incCount, 93 | incFailed, 94 | incPassed, 95 | getValidationMessage, 96 | validations, 97 | timeout, 98 | 99 | extensions: { 100 | ...operators, 101 | ...extensions, 102 | }, 103 | }); 104 | } 105 | 106 | formatter.emit('end', { 107 | count: count(), 108 | failed: failed(), 109 | passed: passed(), 110 | skipped, 111 | }); 112 | 113 | return { 114 | count: count(), 115 | failed: failed(), 116 | passed: passed(), 117 | skipped, 118 | }; 119 | } 120 | 121 | async function runOneTest(options) { 122 | const { 123 | message, 124 | at, 125 | fn, 126 | extensions, 127 | formatter, 128 | count, 129 | total, 130 | failed, 131 | incCount, 132 | incPassed, 133 | incFailed, 134 | getValidationMessage, 135 | validations, 136 | timeout, 137 | } = options; 138 | 139 | const isReturn = fullstore(false); 140 | const assertionsCount = fullstore(0); 141 | const isEnded = fullstore(false); 142 | const incAssertionsCount = inc(assertionsCount); 143 | 144 | formatter.emit('test', { 145 | test: message, 146 | }); 147 | 148 | const {initOperators} = await getInitOperators(); 149 | const {checkIfEnded} = validations; 150 | 151 | const t = initOperators({ 152 | formatter, 153 | count, 154 | incCount, 155 | incPassed, 156 | incFailed, 157 | assertionsCount, 158 | incAssertionsCount, 159 | isEnded, 160 | extensions, 161 | checkIfEnded, 162 | }); 163 | 164 | if (!isReturn()) { 165 | const [timer, stopTimer] = doTimeout(timeout, ['timeout']); 166 | const [error] = await Promise.race([tryToCatch(fn, t), timer]); 167 | 168 | stopTimer(); 169 | isEnded(false); 170 | 171 | if (error) { 172 | t.fail(error, at); 173 | t.end(); 174 | isReturn(true); 175 | } 176 | } 177 | 178 | if (!isReturn()) { 179 | const [validationMessage, atLine] = getValidationMessage(message, { 180 | assertionsCount: assertionsCount(), 181 | }); 182 | 183 | if (atLine) { 184 | t.fail(validationMessage, atLine); 185 | t.end(); 186 | } 187 | } 188 | 189 | formatter.emit('test:end', { 190 | count: count(), 191 | total, 192 | test: message, 193 | failed: failed(), 194 | }); 195 | } 196 | 197 | module.exports.parseTime = parseTime; 198 | -------------------------------------------------------------------------------- /packages/supertape/lib/worker/create-console-log.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {test, stub} = require('supertape'); 4 | 5 | const {stringify} = require('flatted'); 6 | const { 7 | overrideConsoleLog, 8 | CONSOLE_LOG, 9 | overrideConsoleError, 10 | } = require('./create-console-log'); 11 | 12 | test('supertape: worker: create-console-log', (t) => { 13 | const log = stub(); 14 | const consoleStub = { 15 | log, 16 | }; 17 | 18 | const parentPort = { 19 | postMessage: stub(), 20 | }; 21 | 22 | overrideConsoleLog(parentPort, { 23 | console: consoleStub, 24 | }); 25 | 26 | consoleStub.log('hello'); 27 | 28 | t.notCalled(log); 29 | t.end(); 30 | }); 31 | 32 | test('supertape: worker: create-console-log: get back', (t) => { 33 | const log = stub(); 34 | const consoleStub = { 35 | log, 36 | }; 37 | 38 | const parentPort = { 39 | postMessage: stub(), 40 | }; 41 | 42 | const {getBackConsoleLog} = overrideConsoleLog(parentPort, { 43 | console: consoleStub, 44 | }); 45 | 46 | consoleStub.log('hello'); 47 | 48 | getBackConsoleLog(); 49 | 50 | consoleStub.log('world'); 51 | 52 | t.calledWith(log, ['world']); 53 | t.end(); 54 | }); 55 | 56 | test('supertape: worker: create-console-log: get back: consoleError', (t) => { 57 | const error = stub(); 58 | const consoleStub = { 59 | error, 60 | }; 61 | 62 | const parentPort = { 63 | postMessage: stub(), 64 | }; 65 | 66 | const {getBackConsoleError} = overrideConsoleError(parentPort, { 67 | console: consoleStub, 68 | }); 69 | 70 | consoleStub.error('hello'); 71 | 72 | getBackConsoleError(); 73 | 74 | consoleStub.error('world'); 75 | 76 | t.calledWith(error, ['world']); 77 | t.end(); 78 | }); 79 | 80 | test('supertape: worker: create-console-log: postMessage', (t) => { 81 | const log = stub(); 82 | const consoleStub = { 83 | log, 84 | }; 85 | 86 | const postMessage = stub(); 87 | 88 | const parentPort = { 89 | postMessage, 90 | }; 91 | 92 | overrideConsoleLog(parentPort, { 93 | console: consoleStub, 94 | }); 95 | 96 | consoleStub.log('hello'); 97 | 98 | const arg = [ 99 | CONSOLE_LOG, { 100 | message: '["hello"]', 101 | }, 102 | ]; 103 | 104 | t.calledWith(postMessage, [arg]); 105 | t.end(); 106 | }); 107 | 108 | test('supertape: worker: create-console-log: postMessage: array', (t) => { 109 | const log = stub(); 110 | const consoleStub = { 111 | log, 112 | }; 113 | 114 | const postMessage = stub(); 115 | 116 | const parentPort = { 117 | postMessage, 118 | }; 119 | 120 | const {getBackConsoleLog} = overrideConsoleLog(parentPort, { 121 | console: consoleStub, 122 | }); 123 | 124 | consoleStub.log([1, 2]); 125 | 126 | getBackConsoleLog(); 127 | 128 | const arg = [ 129 | CONSOLE_LOG, { 130 | message: '[[1,2]]', 131 | }, 132 | ]; 133 | 134 | t.calledWith(postMessage, [arg]); 135 | t.end(); 136 | }); 137 | 138 | test('supertape: worker: create-console-log: postMessage: object', (t) => { 139 | const log = stub(); 140 | const consoleStub = { 141 | log, 142 | }; 143 | 144 | const postMessage = stub(); 145 | 146 | const parentPort = { 147 | postMessage, 148 | }; 149 | 150 | overrideConsoleLog(parentPort, { 151 | console: consoleStub, 152 | }); 153 | 154 | const objectB = {}; 155 | 156 | const objectA = { 157 | objectB, 158 | }; 159 | 160 | objectA.objectB = { 161 | objectA, 162 | }; 163 | 164 | consoleStub.log(objectA); 165 | 166 | const arg = [ 167 | CONSOLE_LOG, { 168 | message: stringify(objectA), 169 | }, 170 | ]; 171 | 172 | t.calledWith(postMessage, [arg]); 173 | t.end(); 174 | }); 175 | 176 | test('supertape: worker: create-console-log: postMessage: simple object', (t) => { 177 | const log = stub(); 178 | const consoleStub = { 179 | log, 180 | }; 181 | 182 | const postMessage = stub(); 183 | 184 | const parentPort = { 185 | postMessage, 186 | }; 187 | 188 | overrideConsoleLog(parentPort, { 189 | console: consoleStub, 190 | }); 191 | 192 | consoleStub.log({ 193 | hello: 'world', 194 | }); 195 | 196 | const arg = [ 197 | CONSOLE_LOG, { 198 | message: '[{"hello":"1"},"world"]', 199 | }, 200 | ]; 201 | 202 | t.calledWith(postMessage, [arg]); 203 | t.end(); 204 | }); 205 | 206 | test('supertape: worker: create-console-log: postMessage: error', (t) => { 207 | const log = stub(); 208 | const consoleStub = { 209 | log, 210 | }; 211 | 212 | const postMessage = stub(); 213 | 214 | const parentPort = { 215 | postMessage, 216 | }; 217 | 218 | overrideConsoleLog(parentPort, { 219 | console: consoleStub, 220 | }); 221 | 222 | consoleStub.log(Error('hello')); 223 | 224 | const arg = [ 225 | CONSOLE_LOG, { 226 | message: '["Error: hello"]', 227 | }, 228 | ]; 229 | 230 | t.calledWith(postMessage, [arg]); 231 | t.end(); 232 | }); 233 | -------------------------------------------------------------------------------- /packages/formatter-progress-bar/lib/progress-bar.js: -------------------------------------------------------------------------------- 1 | import {Writable} from 'node:stream'; 2 | import process from 'node:process'; 3 | import cliProgress from 'cli-progress'; 4 | import chalk from 'chalk'; 5 | import fullstore from 'fullstore'; 6 | import {isCI} from 'ci-info'; 7 | 8 | global._isCI = isCI; 9 | 10 | const OK = '👌'; 11 | const WARNING = '⚠️'; 12 | const YELLOW = '#f9d472'; 13 | 14 | const {red} = chalk; 15 | const formatErrorsCount = (a) => a ? red(a) : OK; 16 | 17 | const isStr = (a) => typeof a === 'string'; 18 | const {stderr} = process; 19 | 20 | let SUPERTAPE_PROGRESS_BAR; 21 | let SUPERTAPE_PROGRESS_BAR_MIN = 100; 22 | let SUPERTAPE_PROGRESS_BAR_COLOR; 23 | let SUPERTAPE_PROGRESS_BAR_STACK = 1; 24 | 25 | export function createFormatter(bar) { 26 | ({ 27 | SUPERTAPE_PROGRESS_BAR, 28 | SUPERTAPE_PROGRESS_BAR_MIN = 100, 29 | SUPERTAPE_PROGRESS_BAR_COLOR, 30 | SUPERTAPE_PROGRESS_BAR_STACK = 1, 31 | } = process.env); 32 | 33 | const out = createOutput(); 34 | const store = fullstore(); 35 | const barStore = fullstore(bar); 36 | 37 | return { 38 | start: start({ 39 | barStore, 40 | out, 41 | }), 42 | test: test({ 43 | store, 44 | }), 45 | testEnd: testEnd({ 46 | barStore, 47 | }), 48 | fail: fail({ 49 | out, 50 | store, 51 | }), 52 | comment: comment({ 53 | out, 54 | }), 55 | end: end({ 56 | barStore, 57 | out, 58 | }), 59 | }; 60 | } 61 | 62 | export const start = ({barStore, out}) => ({total}) => { 63 | out('TAP version 13'); 64 | 65 | const color = SUPERTAPE_PROGRESS_BAR_COLOR || YELLOW; 66 | const bar = barStore() || _createProgress({ 67 | total, 68 | color, 69 | test: '', 70 | }); 71 | 72 | barStore(bar); 73 | }; 74 | 75 | export const test = ({store}) => ({test}) => { 76 | store(`# ${test}`); 77 | }; 78 | 79 | export const testEnd = ({barStore}) => ({count, total, failed, test}) => { 80 | barStore().increment({ 81 | count, 82 | total, 83 | test, 84 | failed: formatErrorsCount(failed), 85 | }); 86 | }; 87 | 88 | export const fail = ({out, store}) => ({at, count, message, operator, result, expected, output, errorStack}) => { 89 | out(''); 90 | out(store()); 91 | out(`❌ not ok ${count} ${message}`); 92 | out(' ---'); 93 | out(` operator: ${operator}`); 94 | 95 | if (output) 96 | out(output); 97 | 98 | if (!isStr(output)) { 99 | out(' expected: |-'); 100 | out(` ${expected}`); 101 | out(' result: |-'); 102 | out(` ${result}`); 103 | } 104 | 105 | out(` ${at}`); 106 | 107 | if (SUPERTAPE_PROGRESS_BAR_STACK !== '0') { 108 | out(' stack: |-'); 109 | out(errorStack); 110 | } 111 | 112 | out(' ...'); 113 | out(''); 114 | }; 115 | 116 | export const comment = ({out}) => ({message}) => { 117 | out(`# ${message}`); 118 | }; 119 | 120 | export const end = ({barStore, out}) => ({count, passed, failed, skipped}) => { 121 | barStore().stop(); 122 | 123 | out(''); 124 | 125 | out(`1..${count}`); 126 | out(`# tests ${count}`); 127 | out(`# pass ${passed}`); 128 | 129 | if (skipped) 130 | out(formatSkip(skipped)); 131 | 132 | out(''); 133 | 134 | if (failed) 135 | out(`# ❌ fail ${failed}`); 136 | 137 | if (!failed) 138 | out(formatOk()); 139 | 140 | out(''); 141 | out(''); 142 | 143 | return `\r${out()}`; 144 | }; 145 | 146 | function createOutput() { 147 | let output = []; 148 | 149 | return (...args) => { 150 | const [line] = args; 151 | 152 | if (!args.length) { 153 | const result = output.join('\n'); 154 | 155 | output = []; 156 | 157 | return result; 158 | } 159 | 160 | output.push(line); 161 | }; 162 | } 163 | 164 | const getColorFn = (color) => { 165 | if (color.startsWith('#')) 166 | return chalk.hex(color); 167 | 168 | return chalk[color]; 169 | }; 170 | 171 | const defaultStreamOptions = { 172 | total: Infinity, 173 | }; 174 | 175 | const getStream = ({total} = defaultStreamOptions) => { 176 | const is = total >= SUPERTAPE_PROGRESS_BAR_MIN; 177 | 178 | if (is && !global._isCI || SUPERTAPE_PROGRESS_BAR === '1') 179 | return stderr; 180 | 181 | return new Writable(); 182 | }; 183 | 184 | export const _getStream = getStream; 185 | 186 | function _createProgress({total, color, test}) { 187 | const colorFn = getColorFn(color); 188 | const bar = new cliProgress.SingleBar({ 189 | format: `${colorFn('{bar}')} {percentage}% | {failed} | {count}/{total} | {test}`, 190 | barCompleteChar: '\u2588', 191 | barIncompleteChar: '\u2591', 192 | clearOnComplete: true, 193 | stopOnComplete: true, 194 | hideCursor: true, 195 | stream: getStream({ 196 | total, 197 | }), 198 | }, cliProgress.Presets.react); 199 | 200 | bar.start(total, 0, { 201 | test, 202 | total, 203 | count: 0, 204 | failed: OK, 205 | }); 206 | 207 | return bar; 208 | } 209 | 210 | function formatOk() { 211 | const {TERMINAL_EMULATOR} = process.env; 212 | const spaces = /JetBrains/.test(TERMINAL_EMULATOR) ? ' ' : ''; 213 | 214 | return `# ✅ ${spaces}ok`; 215 | } 216 | 217 | const formatSkip = (skipped) => `# ${WARNING} skip ${skipped}`; 218 | -------------------------------------------------------------------------------- /packages/operator-stub/lib/stub.js: -------------------------------------------------------------------------------- 1 | import {isStub} from '@cloudcmd/stub'; 2 | 3 | const isString = (a) => typeof a === 'string'; 4 | const getExpectedStubMessage = (fn) => `Expected stub, but received: ${fn?.toString()}`; 5 | 6 | const {stringify} = JSON; 7 | const {isArray} = Array; 8 | 9 | export const called = (operator) => (fn) => { 10 | if (!isStub(fn)) 11 | return operator.fail(getExpectedStubMessage(fn)); 12 | 13 | return operator.fail(`'t.called' is too general, looks like you need 't.calledWith' or 't.calledWithNoArgs'`); 14 | }; 15 | 16 | export const notCalled = (operator) => (fn, message = 'should not be called') => { 17 | if (!isStub(fn)) 18 | return operator.fail(getExpectedStubMessage(fn)); 19 | 20 | if (!isString(message)) 21 | return operator.fail(`'t.notCalled' expects message to be string, but received: '${stringify(message)}', looks like you need 't.notCalledWith'`); 22 | 23 | return operator.notOk(fn.called, message); 24 | }; 25 | 26 | export const calledWith = (operator) => (fn, args, message = `should be called with 'args'`) => { 27 | if (!isStub(fn)) 28 | return operator.fail(getExpectedStubMessage(fn)); 29 | 30 | if (!fn.called) 31 | return operator.fail(`Expected function to be called with arguments, but not called at all`); 32 | 33 | if (!args) 34 | return operator.fail(`You haven't provided 'args', looks like you need 't.calledWithNoArgs()'`); 35 | 36 | if (!isArray(args)) 37 | return operator.fail(`Expected 'args' to be 'array' but received: ${stringify(args)}`); 38 | 39 | const calledArgs = fn.args.at(-1); 40 | 41 | return operator.deepEqual(calledArgs, args, message); 42 | }; 43 | 44 | export const calledWithNoArgs = (operator) => (fn, message = 'should be called with no arguments') => { 45 | if (!isStub(fn)) 46 | return operator.fail(getExpectedStubMessage(fn)); 47 | 48 | if (!fn.called) 49 | return operator.fail('Expected function to be called with no arguments, but not called at all'); 50 | 51 | if (!isString(message)) 52 | return operator.fail(`'t.calledWithNoArgs' expects message to be string, but received: '${stringify(message)}', looks like you need 't.calledWith'`); 53 | 54 | const calledArgs = fn.args.at(-1); 55 | 56 | return operator.deepEqual(calledArgs, [], message); 57 | }; 58 | 59 | export const calledCount = (operator) => (fn, count, message = 'should be called') => { 60 | if (!isStub(fn)) 61 | return operator.fail(getExpectedStubMessage(fn)); 62 | 63 | return operator.equal(fn.callCount, count, message); 64 | }; 65 | 66 | export const calledOnce = (operator) => (fn, message = 'should be called once') => calledCount(operator)(fn, 1, message); 67 | 68 | export const calledTwice = (operator) => (fn, count, message = 'should be called twice') => calledCount(operator)(fn, 2, message); 69 | 70 | export const calledWithNew = (operator) => (fn, message = 'should be called with new') => { 71 | if (!isStub(fn)) 72 | return operator.fail(getExpectedStubMessage(fn)); 73 | 74 | return operator.ok(fn.calledWithNew(), message); 75 | }; 76 | 77 | const noName = ({name}) => name === 'anonymous'; 78 | 79 | export const calledBefore = (operator) => (fn1, fn2, message) => { 80 | message = message || `should call '${fn1.name}' before '${fn2.name}'`; 81 | 82 | if (!isStub(fn1)) 83 | return operator.fail(getExpectedStubMessage(fn1)); 84 | 85 | if (!isStub(fn2)) 86 | return operator.fail(getExpectedStubMessage(fn2)); 87 | 88 | if (noName(fn1) || noName(fn2)) 89 | return operator.fail(`Looks like you forget to define name of a stub, use: stub().withName('functionName')`); 90 | 91 | if (!fn1.called) 92 | return operator.fail(`Expected function '${fn1.name}' to be called before '${fn2.name}' but '${fn1.name}' not called at all`); 93 | 94 | if (!fn2.called) 95 | return operator.fail(`Expected function '${fn1.name}' to be called before '${fn2.name}' but '${fn2.name}' not called at all`); 96 | 97 | return operator.ok(fn1.calledBefore(fn2), message); 98 | }; 99 | 100 | export const calledAfter = (operator) => (fn1, fn2, message) => { 101 | message = message || `should call '${fn1.name}' after '${fn2.name}'`; 102 | 103 | if (!isStub(fn1)) 104 | return operator.fail(getExpectedStubMessage(fn1)); 105 | 106 | if (!isStub(fn2)) 107 | return operator.fail(getExpectedStubMessage(fn2)); 108 | 109 | if (noName(fn1) || noName(fn2)) 110 | return operator.fail(`Looks like you forget to define name of a stub, use: stub().withName('functionName')`); 111 | 112 | if (!fn1.called) 113 | return operator.fail(`Expected function '${fn1.name}' to be called after '${fn2.name}' but '${fn1.name}' not called at all`); 114 | 115 | if (!fn2.called) 116 | return operator.fail(`Expected function '${fn1.name}' to be called after '${fn2.name}' but '${fn2.name}' not called at all`); 117 | 118 | return operator.ok(fn1.calledAfter(fn2), message); 119 | }; 120 | 121 | export const calledInOrder = (operator) => (fns, message = 'should call in order') => { 122 | if (!isArray(fns)) 123 | return operator.fail(`Expected 'fns' to be 'array' but received: ${fns}`); 124 | 125 | let failMessage = ''; 126 | const fail = (message) => failMessage = message; 127 | 128 | const ok = (result, message) => { 129 | if (!result) 130 | failMessage = message; 131 | }; 132 | 133 | const check = calledBefore({ 134 | fail, 135 | ok, 136 | }); 137 | 138 | for (let i = 0; i < fns.length - 1; i++) { 139 | const fn1 = fns[i]; 140 | const fn2 = fns[i + 1]; 141 | 142 | check(fn1, fn2); 143 | 144 | if (failMessage) 145 | return operator.fail(failMessage); 146 | } 147 | 148 | return operator.pass(message); 149 | }; 150 | -------------------------------------------------------------------------------- /packages/formatter-short/lib/short.spec.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import montag from 'montag'; 3 | import pullout from 'pullout'; 4 | import {test, createTest} from 'supertape'; 5 | import * as shortFormatter from './short.js'; 6 | 7 | const pull = async (stream, i = 9) => { 8 | const output = await pullout(await stream); 9 | 10 | return output 11 | .split('\n') 12 | .slice(0, i) 13 | .join('\n'); 14 | }; 15 | 16 | test('supertape: format: short', async (t) => { 17 | const successFn = (t) => { 18 | t.ok(true); 19 | t.end(); 20 | }; 21 | 22 | const successMessage = 'format: success'; 23 | 24 | const tapFn = (t) => { 25 | t.ok(false); 26 | t.end(); 27 | }; 28 | 29 | const tapMessage = 'format: short'; 30 | 31 | const { 32 | test, 33 | stream, 34 | run, 35 | } = await createTest({ 36 | format: shortFormatter, 37 | }); 38 | 39 | test(successMessage, successFn); 40 | test(tapMessage, tapFn); 41 | 42 | const [result] = await Promise.all([ 43 | pull(stream, 11), 44 | run(), 45 | ]); 46 | 47 | const expected = montag` 48 | TAP version 13 49 | # format: success 50 | ok 1 should be truthy 51 | # format: short 52 | not ok 2 should be truthy 53 | --- 54 | operator: ok 55 | expected: |- 56 | true 57 | result: |- 58 | false 59 | `; 60 | 61 | t.equal(result, expected); 62 | t.end(); 63 | }); 64 | 65 | test('supertape: format: short: skip', async (t) => { 66 | const fn = (t) => { 67 | t.ok(true); 68 | t.end(); 69 | }; 70 | 71 | const message = 'success'; 72 | 73 | process.env.SUPERTAPE_NO_PROGRESS_BAR = 1; 74 | 75 | const { 76 | test, 77 | stream, 78 | run, 79 | } = await createTest({ 80 | format: shortFormatter, 81 | }); 82 | 83 | test(message, fn, { 84 | skip: true, 85 | }); 86 | 87 | const [result] = await Promise.all([ 88 | pull(stream, 8), 89 | run(), 90 | ]); 91 | 92 | delete process.env.SUPERTAPE_NO_PROGRESS_BAR; 93 | 94 | const expected = montag` 95 | TAP version 13 96 | 97 | 1..0 98 | # tests 0 99 | # pass 0 100 | # skip 1 101 | 102 | # ok 103 | `; 104 | 105 | t.equal(result, expected); 106 | t.end(); 107 | }); 108 | 109 | test('supertape: format: short: comment', async (t) => { 110 | const successFn = (t) => { 111 | t.ok(true); 112 | t.end(); 113 | }; 114 | 115 | const successMessage = 'format: success'; 116 | 117 | const tapFn = (t) => { 118 | t.comment('hello'); 119 | t.ok(false); 120 | t.end(); 121 | }; 122 | 123 | const tapMessage = 'format: short'; 124 | 125 | const { 126 | test, 127 | stream, 128 | run, 129 | } = await createTest({ 130 | formatter: shortFormatter, 131 | }); 132 | 133 | test(successMessage, successFn); 134 | test(tapMessage, tapFn); 135 | 136 | const [result] = await Promise.all([ 137 | pull(stream, 12), 138 | run(), 139 | ]); 140 | 141 | const expected = montag` 142 | TAP version 13 143 | # format: success 144 | ok 1 should be truthy 145 | # format: short 146 | # hello 147 | not ok 2 should be truthy 148 | --- 149 | operator: ok 150 | expected: |- 151 | true 152 | result: |- 153 | false 154 | `; 155 | 156 | t.equal(result, expected); 157 | t.end(); 158 | }); 159 | 160 | test('supertape: formatter: short: output', async (t) => { 161 | const fn = (t) => { 162 | t.equal('hello', 'world'); 163 | t.end(); 164 | }; 165 | 166 | const message = 'hello world'; 167 | const { 168 | test, 169 | stream, 170 | run, 171 | } = await createTest({ 172 | formatter: shortFormatter, 173 | }); 174 | 175 | await test(message, fn); 176 | 177 | const BEFORE_DIFF = 6; 178 | const [result] = await Promise.all([ 179 | pull(stream, BEFORE_DIFF), 180 | run(), 181 | ]); 182 | 183 | const expected = montag` 184 | TAP version 13 185 | # hello world 186 | not ok 1 should equal 187 | --- 188 | operator: equal 189 | diff: |- 190 | `; 191 | 192 | t.equal(result, expected); 193 | t.end(); 194 | }); 195 | 196 | test('supertape: format: short: no stack trace', async (t) => { 197 | const successFn = (t) => { 198 | t.ok(true); 199 | t.end(); 200 | }; 201 | 202 | const successMessage = 'format: success'; 203 | 204 | const tapFn = (t) => { 205 | t.ok(false); 206 | t.end(); 207 | }; 208 | 209 | const tapMessage = 'format: short'; 210 | 211 | const { 212 | test, 213 | stream, 214 | run, 215 | } = await createTest({ 216 | formatter: shortFormatter, 217 | }); 218 | 219 | test(successMessage, successFn); 220 | test(tapMessage, tapFn); 221 | 222 | const [output] = await Promise.all([ 223 | pull(stream, 18), 224 | run(), 225 | ]); 226 | 227 | const result = output.replace(/at .+\n/, 'at xxx\n'); 228 | 229 | const expected = montag` 230 | TAP version 13 231 | # format: success 232 | ok 1 should be truthy 233 | # format: short 234 | not ok 2 should be truthy 235 | --- 236 | operator: ok 237 | expected: |- 238 | true 239 | result: |- 240 | false 241 | at xxx 242 | ... 243 | 244 | 1..2 245 | # tests 2 246 | # pass 1 247 | # fail 1 248 | `; 249 | 250 | t.equal(result, expected); 251 | t.end(); 252 | }); 253 | -------------------------------------------------------------------------------- /packages/formatter-time/lib/time.js: -------------------------------------------------------------------------------- 1 | import {Writable} from 'node:stream'; 2 | import process from 'node:process'; 3 | import cliProgress from 'cli-progress'; 4 | import chalk from 'chalk'; 5 | import fullstore from 'fullstore'; 6 | import {isCI} from 'ci-info'; 7 | import {Timer} from 'timer-node'; 8 | import once from 'once'; 9 | 10 | global._isCI = isCI; 11 | 12 | const OK = '👌'; 13 | const YELLOW = '#218bff'; 14 | 15 | const {red} = chalk; 16 | const formatErrorsCount = (a) => a ? red(a) : OK; 17 | 18 | const isStr = (a) => typeof a === 'string'; 19 | 20 | const {stderr} = process; 21 | 22 | const createProgress = once(_createProgress); 23 | 24 | let SUPERTAPE_TIME; 25 | let SUPERTAPE_TIME_MIN = 100; 26 | let SUPERTAPE_TIME_COLOR; 27 | let SUPERTAPE_TIME_STACK = 1; 28 | let SUPERTAPE_TIME_CLOCK = '⏳'; 29 | 30 | export function createFormatter(bar) { 31 | ({ 32 | SUPERTAPE_TIME, 33 | SUPERTAPE_TIME_MIN = 100, 34 | SUPERTAPE_TIME_COLOR, 35 | SUPERTAPE_TIME_STACK = 1, 36 | SUPERTAPE_TIME_CLOCK = '⏳', 37 | } = process.env); 38 | 39 | const out = createOutput(); 40 | const store = fullstore(); 41 | const barStore = fullstore(bar); 42 | const timerStore = fullstore(); 43 | 44 | return { 45 | start: start({ 46 | barStore, 47 | timerStore, 48 | out, 49 | }), 50 | test: test({ 51 | store, 52 | }), 53 | testEnd: testEnd({ 54 | clock: SUPERTAPE_TIME_CLOCK, 55 | barStore, 56 | timerStore, 57 | }), 58 | fail: fail({ 59 | out, 60 | store, 61 | }), 62 | end: end({ 63 | barStore, 64 | out, 65 | }), 66 | }; 67 | } 68 | 69 | export const start = ({barStore, timerStore, out}) => ({total}) => { 70 | out('TAP version 13'); 71 | 72 | const color = SUPERTAPE_TIME_COLOR || YELLOW; 73 | const {bar, timer} = createProgress({ 74 | total, 75 | color, 76 | test: '', 77 | }); 78 | 79 | barStore(bar); 80 | timerStore(timer); 81 | }; 82 | 83 | export const test = ({store}) => ({test}) => { 84 | store(`# ${test}`); 85 | }; 86 | 87 | export const testEnd = ({barStore, clock, timerStore}) => ({count, total, failed, test}) => { 88 | const timer = timerStore(); 89 | 90 | barStore().increment({ 91 | count, 92 | total, 93 | test, 94 | failed: formatErrorsCount(failed), 95 | time: !timer ? '' : getTime({ 96 | clock, 97 | timer: timerStore(), 98 | }), 99 | }); 100 | }; 101 | 102 | export const fail = ({out, store}) => ({at, count, message, operator, result, expected, output, errorStack}) => { 103 | out(''); 104 | out(store()); 105 | out(`❌ not ok ${count} ${message}`); 106 | out(' ---'); 107 | out(` operator: ${operator}`); 108 | 109 | if (output) 110 | out(output); 111 | 112 | if (!isStr(output)) { 113 | out(' expected: |-'); 114 | out(` ${expected}`); 115 | out(' result: |-'); 116 | out(` ${result}`); 117 | } 118 | 119 | out(` ${at}`); 120 | 121 | if (SUPERTAPE_TIME_STACK !== '0') { 122 | out(' stack: |-'); 123 | out(errorStack); 124 | } 125 | 126 | out(' ...'); 127 | out(''); 128 | }; 129 | 130 | export const end = ({barStore, out}) => ({count, passed, failed, skipped}) => { 131 | barStore().stop(); 132 | 133 | out(''); 134 | 135 | out(`1..${count}`); 136 | out(`# tests ${count}`); 137 | out(`# pass ${passed}`); 138 | 139 | if (skipped) 140 | out(`# ⚠️ skip ${skipped}`); 141 | 142 | out(''); 143 | 144 | if (failed) 145 | out(`# ❌ fail ${failed}`); 146 | 147 | if (!failed) 148 | out('# ✅ ok'); 149 | 150 | out(''); 151 | out(''); 152 | 153 | return `\r${out()}`; 154 | }; 155 | 156 | function createOutput() { 157 | let output = []; 158 | 159 | return (...args) => { 160 | const [line] = args; 161 | 162 | if (!args.length) { 163 | const result = output.join('\n'); 164 | 165 | output = []; 166 | 167 | return result; 168 | } 169 | 170 | output.push(line); 171 | }; 172 | } 173 | 174 | export const getColorFn = (color) => { 175 | if (color.startsWith('#')) 176 | return chalk.hex(color); 177 | 178 | return chalk[color]; 179 | }; 180 | 181 | const defaultStreamOptions = { 182 | total: Infinity, 183 | }; 184 | 185 | const getStream = ({total} = defaultStreamOptions) => { 186 | const is = total >= SUPERTAPE_TIME_MIN; 187 | 188 | if (is && !global._isCI || SUPERTAPE_TIME === '1') 189 | return stderr; 190 | 191 | return new Writable(); 192 | }; 193 | 194 | export const _getStream = getStream; 195 | 196 | function _createProgress({total, color, test}) { 197 | const timer = new Timer({ 198 | label: 'supertape-timer', 199 | }); 200 | 201 | const colorFn = getColorFn(color); 202 | const bar = new cliProgress.SingleBar({ 203 | format: `${colorFn('{bar}')} {percentage}% | {failed} | {count}/{total} | {time} | {test}`, 204 | barCompleteChar: '\u2588', 205 | barIncompleteChar: '\u2591', 206 | clearOnComplete: true, 207 | stopOnComplete: true, 208 | hideCursor: true, 209 | stream: getStream({ 210 | total, 211 | }), 212 | }, cliProgress.Presets.react); 213 | 214 | bar.start(total, 0, { 215 | test, 216 | total, 217 | count: 0, 218 | failed: OK, 219 | time: getTime({ 220 | clock: SUPERTAPE_TIME_CLOCK, 221 | timer, 222 | }), 223 | }); 224 | 225 | timer.start(); 226 | 227 | return { 228 | bar, 229 | timer, 230 | }; 231 | } 232 | 233 | export const maybeZero = (a) => a <= 9 ? '0' : ''; 234 | 235 | function getTime({clock, timer}) { 236 | const {m, s} = timer.time(); 237 | const minute = `${maybeZero(m)}${m}`; 238 | const second = `${maybeZero(s)}${s}`; 239 | 240 | return `${clock} ${minute}:${second}`; 241 | } 242 | -------------------------------------------------------------------------------- /packages/supertape/lib/supertape.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const process = require('node:process'); 4 | const {EventEmitter} = require('node:events'); 5 | const {PassThrough} = require('node:stream'); 6 | 7 | const stub = require('@cloudcmd/stub'); 8 | const once = require('once'); 9 | 10 | const options = require('../supertape.json'); 11 | 12 | const {getAt, setValidations} = require('./validator'); 13 | const runTests = require('./run-tests'); 14 | const _createFormatter = require('./formatter').createFormatter; 15 | const createFormatter = once(_createFormatter); 16 | 17 | const createEmitter = once(_createEmitter); 18 | 19 | const {assign} = Object; 20 | const {stdout} = process; 21 | 22 | // 5ms ought to be enough for anybody 23 | const { 24 | SUPERTAPE_LOAD_LOOP_TIMEOUT = 5, 25 | SUPERTAPE_TIMEOUT = 3000, 26 | } = process.env; 27 | 28 | let mainEmitter; 29 | 30 | const getOperators = once(async () => { 31 | const {operators} = options; 32 | const {loadOperators} = await import('@supertape/engine-loader'); 33 | 34 | return await loadOperators(operators); 35 | }); 36 | 37 | const defaultOptions = { 38 | skip: false, 39 | only: false, 40 | extensions: {}, 41 | quiet: false, 42 | format: 'tap', 43 | run: true, 44 | getOperators, 45 | isStop: () => false, 46 | checkDuplicates: true, 47 | checkIfEnded: true, 48 | checkAssertionsCount: true, 49 | checkScopes: true, 50 | timeout: SUPERTAPE_TIMEOUT, 51 | }; 52 | 53 | function _createEmitter({quiet, stream = stdout, format, getOperators, isStop, readyFormatter, workerFormatter}) { 54 | const tests = []; 55 | const emitter = new EventEmitter(); 56 | 57 | emitter.on('test', (message, fn, {skip, only, extensions, at, validations, timeout}) => { 58 | tests.push({ 59 | message, 60 | fn, 61 | skip, 62 | only, 63 | extensions, 64 | at, 65 | validations, 66 | timeout, 67 | }); 68 | }); 69 | 70 | emitter.on('loop', () => { 71 | loop({ 72 | emitter, 73 | tests, 74 | }); 75 | }); 76 | 77 | emitter.on('run', async () => { 78 | const {harness, formatter} = readyFormatter || await createFormatter(format); 79 | 80 | if (!quiet) 81 | harness.pipe(stream); 82 | 83 | const operators = await getOperators(); 84 | 85 | const result = await runTests(tests, { 86 | formatter: workerFormatter || formatter, 87 | operators, 88 | isStop, 89 | }); 90 | 91 | emitter.emit('end', result); 92 | }); 93 | 94 | return emitter; 95 | } 96 | 97 | module.exports = test; 98 | 99 | const initedOptions = { 100 | format: 'tap', 101 | }; 102 | 103 | module.exports.init = (options) => { 104 | assign(initedOptions, options); 105 | }; 106 | 107 | const createStream = async () => { 108 | const {format} = initedOptions; 109 | const {harness} = await createFormatter(format); 110 | 111 | return harness; 112 | }; 113 | 114 | module.exports.createStream = createStream; 115 | module.exports.createTest = async (testOptions = {}) => { 116 | const {format = 'tap', formatter} = testOptions; 117 | const readyFormatter = await _createFormatter(formatter || format); 118 | 119 | const stream = new PassThrough(); 120 | const emitter = _createEmitter({ 121 | ...defaultOptions, 122 | ...testOptions, 123 | readyFormatter, 124 | stream, 125 | }); 126 | 127 | const fn = (message, fn, options = {}) => { 128 | return test(message, fn, { 129 | ...testOptions, 130 | ...options, 131 | emitter, 132 | }); 133 | }; 134 | 135 | assign(fn, { 136 | stream, 137 | ...test, 138 | test: fn, 139 | run: () => { 140 | emitter.emit('run'); 141 | }, 142 | }); 143 | 144 | return fn; 145 | }; 146 | 147 | function test(message, fn, options = {}) { 148 | const { 149 | run, 150 | quiet, 151 | format, 152 | only, 153 | skip, 154 | extensions, 155 | getOperators, 156 | isStop, 157 | checkDuplicates, 158 | checkScopes, 159 | checkAssertionsCount, 160 | checkIfEnded, 161 | workerFormatter, 162 | timeout, 163 | } = { 164 | ...defaultOptions, 165 | ...initedOptions, 166 | ...options, 167 | }; 168 | 169 | const validations = { 170 | checkDuplicates, 171 | checkScopes, 172 | checkAssertionsCount, 173 | checkIfEnded, 174 | }; 175 | 176 | setValidations(validations); 177 | 178 | const at = getAt(); 179 | const emitter = options.emitter || createEmitter({ 180 | format, 181 | quiet, 182 | getOperators, 183 | isStop, 184 | workerFormatter, 185 | }); 186 | 187 | mainEmitter = emitter; 188 | 189 | emitter.emit('test', message, fn, { 190 | skip, 191 | only, 192 | extensions, 193 | at, 194 | validations, 195 | timeout, 196 | }); 197 | 198 | if (run) 199 | emitter.emit('loop'); 200 | 201 | return emitter; 202 | } 203 | 204 | test.skip = (message, fn, options) => { 205 | return test(message, fn, { 206 | ...options, 207 | skip: true, 208 | }); 209 | }; 210 | 211 | test.only = (message, fn, options) => { 212 | return test(message, fn, { 213 | ...options, 214 | only: true, 215 | }); 216 | }; 217 | 218 | const getExtend = (extensions, type) => (message, fn, options) => { 219 | return test(message, fn, { 220 | extensions, 221 | ...options, 222 | ...type, 223 | }); 224 | }; 225 | 226 | test.stub = stub; 227 | test.test = test; 228 | 229 | test.extend = (extensions) => { 230 | const extendedTest = getExtend(extensions); 231 | 232 | assign(extendedTest, { 233 | test: extendedTest, 234 | stub, 235 | }); 236 | 237 | extendedTest.only = getExtend(extensions, { 238 | only: true, 239 | }); 240 | 241 | extendedTest.skip = getExtend(extensions, { 242 | skip: true, 243 | }); 244 | 245 | return extendedTest; 246 | }; 247 | 248 | const loop = once(({emitter, tests}) => { 249 | let previousCount = 0; 250 | 251 | (function loop() { 252 | if (previousCount === tests.length) { 253 | emitter.emit('run'); 254 | 255 | return; 256 | } 257 | 258 | previousCount = tests.length; 259 | setTimeout(loop, SUPERTAPE_LOAD_LOOP_TIMEOUT); 260 | })(); 261 | }); 262 | 263 | module.exports.run = () => { 264 | if (!mainEmitter) 265 | return fakeEmitter(); 266 | 267 | mainEmitter.emit('loop'); 268 | 269 | return mainEmitter; 270 | }; 271 | 272 | function fakeEmitter() { 273 | const emitter = new EventEmitter(); 274 | 275 | process.nextTick(() => { 276 | emitter.emit('end', { 277 | failed: 0, 278 | }); 279 | }); 280 | 281 | return emitter; 282 | } 283 | -------------------------------------------------------------------------------- /packages/supertape/lib/operators.mjs: -------------------------------------------------------------------------------- 1 | import {isDeepStrictEqual} from 'node:util'; 2 | import diff from './diff.mjs'; 3 | import {formatOutput, parseAt} from './format.js'; 4 | 5 | const {entries} = Object; 6 | const isAsync = (a) => a[Symbol.toStringTag] === 'AsyncFunction'; 7 | 8 | const encode = (a) => a 9 | .replace('^', '\\^') 10 | .replace(')', '\\)') 11 | .replace('(', '\\('); 12 | 13 | const maybeRegExp = (a) => { 14 | if (!isStr(a)) 15 | return a; 16 | 17 | return RegExp(encode(a)); 18 | }; 19 | 20 | const isFn = (a) => typeof a === 'function'; 21 | const isStr = (a) => typeof a === 'string'; 22 | const isObj = (a) => typeof a === 'object'; 23 | 24 | const end = () => {}; 25 | 26 | const ok = (result, message = 'should be truthy') => ({ 27 | is: Boolean(result), 28 | expected: true, 29 | result, 30 | message, 31 | }); 32 | 33 | const notOk = (result, message = 'should be falsy') => ({ 34 | is: !result, 35 | expected: false, 36 | result: result && stringify(result), 37 | message, 38 | }); 39 | 40 | const validateRegExp = (regexp) => { 41 | if (!isObj(regexp) && !isStr(regexp)) 42 | return Error('regexp should be RegExp or String'); 43 | 44 | if (!regexp) 45 | return Error('regexp cannot be empty'); 46 | 47 | return null; 48 | }; 49 | 50 | const {stringify} = JSON; 51 | 52 | function match(result, regexp, message = 'should match') { 53 | const error = validateRegExp(regexp); 54 | 55 | if (error) 56 | return fail(error); 57 | 58 | const is = maybeRegExp(regexp).test(result); 59 | 60 | return { 61 | is, 62 | result, 63 | expected: regexp, 64 | message, 65 | }; 66 | } 67 | 68 | function notMatch(result, regexp, message = 'should not match') { 69 | const {is} = match(result, regexp, message); 70 | 71 | return { 72 | is: !is, 73 | result, 74 | expected: regexp, 75 | message, 76 | }; 77 | } 78 | 79 | function equal(result, expected, message = 'should equal') { 80 | let output = ''; 81 | const is = Object.is(result, expected); 82 | 83 | if (!is) 84 | output = diff(expected, result) || ' result: values not equal, but deepEqual'; 85 | 86 | return { 87 | is, 88 | result, 89 | expected, 90 | message, 91 | output, 92 | }; 93 | } 94 | 95 | function notEqual(result, expected, message = 'should not equal') { 96 | const is = !Object.is(result, expected); 97 | const output = is ? '' : diff(expected, result); 98 | 99 | return { 100 | is, 101 | result, 102 | expected, 103 | message, 104 | output, 105 | }; 106 | } 107 | 108 | const pass = (message = '(unnamed assert)') => ({ 109 | is: true, 110 | output: '', 111 | message, 112 | }); 113 | 114 | const fail = (error, at) => ({ 115 | is: false, 116 | stack: error.stack, 117 | output: '', 118 | message: error, 119 | at, 120 | }); 121 | 122 | const deepEqual = (result, expected, message = 'should deep equal') => { 123 | const is = isDeepStrictEqual(result, expected); 124 | const output = is ? '' : diff(expected, result); 125 | 126 | return { 127 | is: is || !output, 128 | result, 129 | expected, 130 | message, 131 | output, 132 | }; 133 | }; 134 | 135 | const notDeepEqual = (result, expected, message = 'should not deep equal') => { 136 | const is = !isDeepStrictEqual(result, expected); 137 | const output = is ? '' : diff(expected, result); 138 | 139 | return { 140 | is, 141 | result, 142 | expected, 143 | message, 144 | output, 145 | }; 146 | }; 147 | 148 | const comment = ({formatter}) => (message) => { 149 | const messages = message 150 | .trim() 151 | .split('\n'); 152 | 153 | for (const current of messages) { 154 | const line = current 155 | .trim() 156 | .replace(/^#\s*/, ''); 157 | 158 | formatter.emit('comment', line); 159 | } 160 | }; 161 | 162 | export const operators = { 163 | equal, 164 | notEqual, 165 | deepEqual, 166 | notDeepEqual, 167 | ok, 168 | notOk, 169 | pass, 170 | fail, 171 | end, 172 | match, 173 | notMatch, 174 | }; 175 | 176 | const initOperator = (runnerState) => (name) => { 177 | const fn = operators[name]; 178 | 179 | if (isAsync(fn)) 180 | return async (...a) => { 181 | const [valid, end] = validateEnd({ 182 | name, 183 | operators, 184 | runnerState, 185 | }); 186 | 187 | if (!valid) 188 | return end; 189 | 190 | const testState = await fn(...a); 191 | run(name, runnerState, testState); 192 | 193 | return testState; 194 | }; 195 | 196 | return (...a) => { 197 | const [valid, end] = validateEnd({ 198 | name, 199 | operators, 200 | runnerState, 201 | }); 202 | 203 | if (!valid) 204 | return end; 205 | 206 | const testState = fn(...a); 207 | 208 | if (testState instanceof Promise) 209 | throw Error(`☝️ Looks like test function returned Promise, but it was determined as not async function. Maybe the reason is 'curry', try to create to separate functions instead`); 210 | 211 | run(name, runnerState, testState); 212 | 213 | return testState; 214 | }; 215 | }; 216 | 217 | const VALID = true; 218 | const INVALID = false; 219 | 220 | function validateEnd({name, operators, runnerState}) { 221 | const { 222 | isEnded, 223 | incAssertionsCount, 224 | } = runnerState; 225 | 226 | if (name === 'end' && isEnded()) 227 | return [INVALID, run('fail', runnerState, operators.fail(`Cannot use a couple 't.end()' operators in one test`))]; 228 | 229 | if (name === 'end') { 230 | isEnded(true); 231 | return [INVALID, end]; 232 | } 233 | 234 | incAssertionsCount(); 235 | 236 | if (isEnded()) 237 | return [INVALID, run('fail', runnerState, operators.fail(`Cannot run assertions after 't.end()' called`))]; 238 | 239 | return [VALID]; 240 | } 241 | 242 | const validate = (a) => { 243 | if (isFn(a)) 244 | return fail(`☝️ Looks like operator returns function, it will always fail: '${a}'`); 245 | 246 | return a; 247 | }; 248 | 249 | const returnMissing = () => fail('☝️ Looks like operator returns nothing, it will always fail'); 250 | 251 | function run(name, {formatter, count, incCount, incPassed, incFailed}, testState = returnMissing()) { 252 | const { 253 | is, 254 | message, 255 | expected, 256 | result, 257 | output, 258 | stack, 259 | at, 260 | } = validate(testState); 261 | 262 | incCount(); 263 | 264 | if (is) { 265 | incPassed(); 266 | formatter.emit('test:success', { 267 | count: count(), 268 | message, 269 | }); 270 | 271 | return; 272 | } 273 | 274 | incFailed(); 275 | 276 | const errorStack = stack || Error(message).stack; 277 | const reason = stack ? 'exception' : 'user'; 278 | 279 | formatter.emit('test:fail', { 280 | count: count(), 281 | message, 282 | operator: name, 283 | result, 284 | expected, 285 | output, 286 | errorStack: formatOutput(errorStack), 287 | at: at || parseAt(errorStack, { 288 | reason, 289 | }), 290 | }); 291 | } 292 | 293 | export const initOperators = ({formatter, count, incCount, incPassed, incFailed, incAssertionsCount, isEnded, extensions}) => { 294 | const operator = initOperator({ 295 | formatter, 296 | count, 297 | incCount, 298 | incPassed, 299 | incFailed, 300 | isEnded, 301 | incAssertionsCount, 302 | }); 303 | 304 | const extendedOperators = {}; 305 | 306 | for (const [name, fn] of entries(extensions)) { 307 | operators[name] = fn(operators); 308 | extendedOperators[name] = operator(name); 309 | } 310 | 311 | return { 312 | equal: operator('equal'), 313 | notEqual: operator('notEqual'), 314 | deepEqual: operator('deepEqual'), 315 | notDeepEqual: operator('notDeepEqual'), 316 | ok: operator('ok'), 317 | notOk: operator('notOk'), 318 | pass: operator('pass'), 319 | fail: operator('fail'), 320 | comment: comment({ 321 | formatter, 322 | }), 323 | match: operator('match'), 324 | notMatch: operator('notMatch'), 325 | end: operator('end'), 326 | 327 | ...extendedOperators, 328 | }; 329 | }; 330 | -------------------------------------------------------------------------------- /packages/supertape/lib/operators.spec.mjs: -------------------------------------------------------------------------------- 1 | import {once, EventEmitter} from 'node:events'; 2 | import stub from '@cloudcmd/stub'; 3 | import currify from 'currify'; 4 | import tryCatch from 'try-catch'; 5 | import test from './supertape.js'; 6 | import {initOperators, operators} from './operators.mjs'; 7 | 8 | const noop = () => {}; 9 | const {stringify} = JSON; 10 | 11 | test('supertape: operators: extendOperators', async (t) => { 12 | const extensions = { 13 | transformCode: (t) => (a, b) => { 14 | return t.equal(a, b, 'should transform code'); 15 | }, 16 | }; 17 | 18 | const formatter = new EventEmitter(); 19 | const {transformCode} = initOperators(getStubs({ 20 | formatter, 21 | extensions, 22 | })); 23 | 24 | const [[result]] = await Promise.all([ 25 | once(formatter, 'test:success'), 26 | transformCode('a', 'a'), 27 | ]); 28 | 29 | const expected = { 30 | count: 1, 31 | message: 'should transform code', 32 | }; 33 | 34 | t.deepEqual(result, expected); 35 | t.end(); 36 | }); 37 | 38 | test('supertape: operators: extendOperators: curried promise', async (t) => { 39 | const extensions = { 40 | transformCode: currify(async (t, a, b) => { 41 | return await t.equal(a, b, 'should transform code'); 42 | }), 43 | }; 44 | 45 | const formatter = new EventEmitter(); 46 | const {transformCode} = initOperators(getStubs({ 47 | formatter, 48 | extensions, 49 | })); 50 | 51 | const [error] = tryCatch(transformCode, 'a', 'a'); 52 | const expected = `☝️ Looks like test function returned Promise, but it was determined as not async function. Maybe the reason is 'curry', try to create to separate functions instead`; 53 | 54 | t.equal(error?.message, expected); 55 | t.end(); 56 | }); 57 | 58 | test('supertape: operators: returns', (t) => { 59 | const result = t.equal('a', 'a'); 60 | 61 | t.equal(result.message, 'should equal'); 62 | t.end(); 63 | }, { 64 | checkAssertionsCount: false, 65 | }); 66 | 67 | test('supertape: operators: extendOperators: async: returns', async (t) => { 68 | const extensions = { 69 | transformCode: (t) => async (a, b) => { 70 | return await t.equal(a, b, 'should transform code'); 71 | }, 72 | }; 73 | 74 | const formatter = new EventEmitter(); 75 | const {transformCode} = initOperators(getStubs({ 76 | formatter, 77 | extensions, 78 | })); 79 | 80 | const [result] = await Promise.all([ 81 | transformCode('a', 'a'), 82 | once(formatter, 'test:success'), 83 | ]); 84 | 85 | t.equal(result.message, 'should transform code'); 86 | t.end(); 87 | }); 88 | 89 | test('supertape: operators: initOperators: notEqual', async (t) => { 90 | const formatter = new EventEmitter(); 91 | const {notEqual} = initOperators(getStubs({ 92 | formatter, 93 | })); 94 | 95 | const [[result]] = await Promise.all([ 96 | once(formatter, 'test:success'), 97 | notEqual(+0, -0), 98 | ]); 99 | 100 | const expected = { 101 | count: 1, 102 | message: 'should not equal', 103 | }; 104 | 105 | t.deepEqual(result, expected); 106 | t.end(); 107 | }); 108 | 109 | test('supertape: operators: initOperators: notDeepEqual: true', async (t) => { 110 | const formatter = new EventEmitter(); 111 | const {notDeepEqual} = initOperators(getStubs({ 112 | formatter, 113 | })); 114 | 115 | const [[result]] = await Promise.all([ 116 | once(formatter, 'test:success'), 117 | notDeepEqual({a: 'b'}, { 118 | b: 'a', 119 | }), 120 | ]); 121 | 122 | const expected = { 123 | count: 1, 124 | message: 'should not deep equal', 125 | }; 126 | 127 | t.deepEqual(result, expected); 128 | t.end(); 129 | }); 130 | 131 | test('supertape: operators: deepEqual: no visual differences', async (t) => { 132 | const formatter = new EventEmitter(); 133 | const {deepEqual} = initOperators(getStubs({ 134 | formatter, 135 | })); 136 | 137 | const a = { 138 | fn: noop, 139 | }; 140 | 141 | const b = { 142 | fn: noop, 143 | }; 144 | 145 | const [[result]] = await Promise.all([ 146 | once(formatter, 'test:success'), 147 | deepEqual(a, b), 148 | ]); 149 | 150 | const expected = { 151 | count: 1, 152 | message: 'should deep equal', 153 | }; 154 | 155 | t.deepEqual(result, expected); 156 | t.end(); 157 | }); 158 | 159 | test('supertape: operators: equal', (t) => { 160 | const {equal} = operators; 161 | const {is} = equal(+0, -0); 162 | 163 | t.notOk(is, 'should use Object.is for comparison'); 164 | t.end(); 165 | }); 166 | 167 | test('supertape: operators: notEqual: true', (t) => { 168 | const {notEqual} = operators; 169 | const {is} = notEqual(+0, -0); 170 | 171 | t.ok(is, 'should use Object.is for comparison'); 172 | t.end(); 173 | }); 174 | 175 | test('supertape: operators: notEqual: false', (t) => { 176 | const {notEqual} = operators; 177 | const {is} = notEqual(1, 1); 178 | 179 | t.notOk(is); 180 | t.end(); 181 | }); 182 | 183 | test('supertape: operators: notDeepEqual: false', (t) => { 184 | const {notDeepEqual} = operators; 185 | const {is} = notDeepEqual({a: 'b'}, { 186 | a: 'b', 187 | }); 188 | 189 | t.notOk(is); 190 | t.end(); 191 | }); 192 | 193 | test('supertape: operators: notDeepEqual: true', (t) => { 194 | const {notDeepEqual} = operators; 195 | const {is} = notDeepEqual(1, 1); 196 | 197 | t.notOk(is); 198 | t.end(); 199 | }); 200 | 201 | test('supertape: operators: notOk: false', (t) => { 202 | const {notOk} = operators; 203 | const {expected} = notOk(false); 204 | 205 | t.notOk(expected); 206 | t.end(); 207 | }); 208 | 209 | test('supertape: operators: notOk: object', (t) => { 210 | const {notOk} = operators; 211 | const {result} = notOk({ 212 | hello: 'world', 213 | }); 214 | 215 | const expected = stringify({ 216 | hello: 'world', 217 | }); 218 | 219 | t.equal(result, expected); 220 | t.end(); 221 | }); 222 | 223 | test('supertape: operators: match: not RegExp and String', (t) => { 224 | const {match} = operators; 225 | const {message} = match('hello', 5); 226 | 227 | t.equal(message.message, 'regexp should be RegExp or String'); 228 | t.end(); 229 | }); 230 | 231 | test('supertape: operators: match: ^', (t) => { 232 | const {match} = operators; 233 | const {is} = match('^ hello', '^ hello'); 234 | 235 | t.ok(is); 236 | t.end(); 237 | }); 238 | 239 | test('supertape: operators: match: encode', (t) => { 240 | const {match} = operators; 241 | const line = '"><svg onload=alert(3);>'; 242 | const {is} = match(line, '"><svg onload=alert(3);>'); 243 | 244 | t.ok(is, 'regexp should match'); 245 | t.end(); 246 | }); 247 | 248 | test('supertape: operators: match: empty', (t) => { 249 | const {match} = operators; 250 | const {message} = match('hello', ''); 251 | 252 | t.equal(message.message, 'regexp cannot be empty'); 253 | t.end(); 254 | }); 255 | 256 | test('supertape: operators: match: empty string: not ok', (t) => { 257 | const {match} = operators; 258 | const {expected} = match('hello', ''); 259 | 260 | t.notOk(expected); 261 | t.end(); 262 | }); 263 | 264 | test('supertape: operators: match: string', (t) => { 265 | const {match} = operators; 266 | const {is} = match('hello', 'world'); 267 | 268 | t.notOk(is); 269 | t.end(); 270 | }); 271 | 272 | test('supertape: operators: not match: string', (t) => { 273 | const {notMatch} = operators; 274 | const {is} = notMatch('hello', 'world'); 275 | 276 | t.ok(is); 277 | t.end(); 278 | }); 279 | 280 | test('supertape: operators: not match: message', (t) => { 281 | const {notMatch} = operators; 282 | const {message} = notMatch('hello', 'world'); 283 | 284 | t.equal(message, 'should not match'); 285 | t.end(); 286 | }); 287 | 288 | test('supertape: operators: not match', (t) => { 289 | const {notMatch} = operators; 290 | const {is} = notMatch('hello', /world/); 291 | 292 | t.ok(is); 293 | t.end(); 294 | }); 295 | 296 | test('supertape: operators: match: not', (t) => { 297 | const {match} = operators; 298 | const {is} = match('hello', /world/); 299 | 300 | t.notOk(is); 301 | t.end(); 302 | }); 303 | 304 | test('supertape: operators: match', (t) => { 305 | const {match} = operators; 306 | const {is} = match('hello', /hello/); 307 | 308 | t.ok(is); 309 | t.end(); 310 | }); 311 | 312 | test('supertape: operators: end', (t) => { 313 | const {end} = operators; 314 | const result = end(); 315 | 316 | t.notOk(result); 317 | t.end(); 318 | }); 319 | 320 | function getStubs(stubs = {}) { 321 | const { 322 | formatter = new EventEmitter(), 323 | count = stub().returns(1), 324 | incCount = stub(), 325 | incPassed = stub(), 326 | incFailed = stub(), 327 | incAssertionsCount = stub(), 328 | isEnded = stub(), 329 | extensions = {}, 330 | } = stubs; 331 | 332 | return { 333 | formatter, 334 | count, 335 | incCount, 336 | incPassed, 337 | incFailed, 338 | isEnded, 339 | incAssertionsCount, 340 | extensions, 341 | }; 342 | } 343 | -------------------------------------------------------------------------------- /packages/supertape/lib/validator.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tryCatch = require('try-catch'); 4 | 5 | const mockRequire = require('mock-require'); 6 | const {stub, test} = require('..'); 7 | 8 | const {getAt} = require('./validator'); 9 | 10 | const {stopAll, reRequire} = mockRequire; 11 | 12 | test('supertape: validator: getAt', (t) => { 13 | const message = 'hello'; 14 | const checkDuplicates = true; 15 | const StackTracey = stub().returns({ 16 | items: [], 17 | }); 18 | 19 | mockRequire('stacktracey', StackTracey); 20 | 21 | const {getAt} = reRequire('./validator'); 22 | getAt({ 23 | message, 24 | checkDuplicates, 25 | }); 26 | 27 | const result = getAt({ 28 | message, 29 | checkDuplicates, 30 | }); 31 | 32 | stopAll(); 33 | 34 | t.equal(result, ''); 35 | t.end(); 36 | }); 37 | 38 | test('supertape: validator: getAt: duplicate', (t) => { 39 | const message = 'hello'; 40 | const checkDuplicates = true; 41 | 42 | getAt({ 43 | message, 44 | checkDuplicates, 45 | }); 46 | 47 | const result = getAt({ 48 | message, 49 | checkDuplicates, 50 | }); 51 | 52 | t.match(result, 'at'); 53 | t.end(); 54 | }); 55 | 56 | test('supertape: validator: checkScopes', (t) => { 57 | const { 58 | createValidator, 59 | setValidations, 60 | } = reRequire('./validator'); 61 | 62 | const current = { 63 | message: 'hello world', 64 | at: 'at', 65 | }; 66 | 67 | const tests = [current]; 68 | 69 | setValidations({ 70 | checkScopes: true, 71 | }); 72 | 73 | const validate = createValidator({ 74 | tests, 75 | }); 76 | 77 | const result = validate('hello world'); 78 | 79 | const expected = [ 80 | `Scope should be defined before first colon: 'scope: subject', received: 'hello world'`, 81 | 'at', 82 | ]; 83 | 84 | t.deepEqual(result, expected); 85 | t.end(); 86 | }); 87 | 88 | test('supertape: validator: checkScopes: @', (t) => { 89 | const { 90 | createValidator, 91 | setValidations, 92 | } = reRequire('./validator'); 93 | 94 | const current = { 95 | message: '@putout/eslint: create-plugin', 96 | at: 'at', 97 | }; 98 | 99 | const tests = [current]; 100 | 101 | setValidations({ 102 | checkScopes: true, 103 | }); 104 | 105 | const validate = createValidator({ 106 | tests, 107 | }); 108 | 109 | const result = validate('@putout/eslint: create-plugin'); 110 | const expected = []; 111 | 112 | t.deepEqual(result, expected); 113 | t.end(); 114 | }); 115 | 116 | test('supertape: validator: checkScopes: +', (t) => { 117 | const { 118 | createValidator, 119 | setValidations, 120 | } = reRequire('./validator'); 121 | 122 | const current = { 123 | message: '+hello: world', 124 | at: 'at', 125 | }; 126 | 127 | const tests = [current]; 128 | 129 | setValidations({ 130 | checkScopes: true, 131 | }); 132 | 133 | const validate = createValidator({ 134 | tests, 135 | }); 136 | 137 | const result = validate('+hello: world'); 138 | const expected = []; 139 | 140 | t.deepEqual(result, expected); 141 | t.end(); 142 | }); 143 | 144 | test('supertape: validator: checkAssertionsCount', (t) => { 145 | const { 146 | createValidator, 147 | setValidations, 148 | } = reRequire('./validator'); 149 | 150 | const current = { 151 | message: 'hello world', 152 | at: 'at', 153 | validations: { 154 | checkAssertionsCount: true, 155 | }, 156 | }; 157 | 158 | const tests = [current]; 159 | 160 | setValidations({ 161 | checkAssertionsCount: true, 162 | }); 163 | 164 | const validate = createValidator({ 165 | tests, 166 | }); 167 | 168 | const result = validate('hello world', { 169 | assertionsCount: 2, 170 | }); 171 | 172 | const expected = [ 173 | 'Only one assertion per test allowed, looks like you have more', 174 | 'at', 175 | ]; 176 | 177 | t.deepEqual(result, expected); 178 | t.end(); 179 | }); 180 | 181 | test('supertape: validator: checkAssertionsCount: disabled', (t) => { 182 | const { 183 | createValidator, 184 | setValidations, 185 | } = reRequire('./validator'); 186 | 187 | const current = { 188 | message: 'hello world', 189 | at: 'at', 190 | validations: { 191 | checkAssertionsCount: false, 192 | }, 193 | }; 194 | 195 | const tests = [current]; 196 | 197 | setValidations({ 198 | checkAssertionsCount: true, 199 | }); 200 | 201 | const validate = createValidator({ 202 | tests, 203 | }); 204 | 205 | const result = validate('hello world', { 206 | assertionsCount: 2, 207 | }); 208 | 209 | const expected = []; 210 | 211 | t.deepEqual(result, expected); 212 | t.end(); 213 | }); 214 | 215 | test('supertape: validator: checkAssertionsCount: ok', (t) => { 216 | const { 217 | createValidator, 218 | setValidations, 219 | } = reRequire('./validator'); 220 | 221 | const current = { 222 | message: 'hello world', 223 | at: 'at', 224 | validations: { 225 | checkAssertionsCount: true, 226 | }, 227 | }; 228 | 229 | const tests = [current]; 230 | 231 | setValidations({ 232 | checkAssertionsCount: true, 233 | }); 234 | 235 | const validate = createValidator({ 236 | tests, 237 | }); 238 | 239 | const result = validate('hello world', { 240 | assertionsCount: 1, 241 | }); 242 | 243 | const expected = []; 244 | 245 | t.deepEqual(result, expected); 246 | t.end(); 247 | }); 248 | 249 | test('supertape: validator: checkScopes: valid', (t) => { 250 | const { 251 | createValidator, 252 | setValidations, 253 | } = reRequire('./validator'); 254 | 255 | const current = { 256 | message: 'hello: world', 257 | at: 'at', 258 | }; 259 | 260 | const tests = [current]; 261 | 262 | setValidations({ 263 | checkScopes: true, 264 | }); 265 | 266 | const validate = createValidator({ 267 | tests, 268 | }); 269 | 270 | const result = validate('hello: world'); 271 | const expected = []; 272 | 273 | t.deepEqual(result, expected); 274 | t.end(); 275 | }); 276 | 277 | test('supertape: validator: checkScopes: nested: valid', (t) => { 278 | const { 279 | createValidator, 280 | setValidations, 281 | } = reRequire('./validator'); 282 | 283 | const message = 'hello: world: and: more'; 284 | const current = { 285 | message, 286 | at: 'at', 287 | }; 288 | 289 | const tests = [current]; 290 | 291 | setValidations({ 292 | checkScopes: true, 293 | }); 294 | 295 | const validate = createValidator({ 296 | tests, 297 | }); 298 | 299 | const result = validate(message); 300 | const expected = []; 301 | 302 | t.deepEqual(result, expected); 303 | t.end(); 304 | }); 305 | 306 | test('supertape: validator: checkScopes: nested: slash', (t) => { 307 | const { 308 | createValidator, 309 | setValidations, 310 | } = reRequire('./validator'); 311 | 312 | const message = 'travis/set-node-versions: report: is function'; 313 | const current = { 314 | message, 315 | at: 'at', 316 | }; 317 | 318 | const tests = [current]; 319 | 320 | setValidations({ 321 | checkScopes: true, 322 | }); 323 | 324 | const validate = createValidator({ 325 | tests, 326 | }); 327 | 328 | const result = validate(message); 329 | const expected = []; 330 | 331 | t.deepEqual(result, expected); 332 | t.end(); 333 | }); 334 | 335 | test('supertape: validator: checkScopes: cannot find message', (t) => { 336 | const { 337 | createValidator, 338 | setValidations, 339 | } = reRequire('./validator'); 340 | 341 | const current = { 342 | message: 'hello: world', 343 | at: 'at', 344 | }; 345 | 346 | const tests = [current]; 347 | 348 | setValidations({ 349 | checkScopes: true, 350 | }); 351 | 352 | const validate = createValidator({ 353 | tests, 354 | }); 355 | 356 | const [error] = tryCatch(validate, 'hello world'); 357 | const expected = '☝️Looks like message cannot be find in tests, this should never happen'; 358 | 359 | t.equal(error.message, expected); 360 | t.end(); 361 | }); 362 | 363 | test('supertape: validator: no validations', (t) => { 364 | const { 365 | createValidator, 366 | setValidations, 367 | } = reRequire('./validator'); 368 | 369 | const current = { 370 | message: 'hello world', 371 | at: 'at', 372 | }; 373 | 374 | const tests = [current]; 375 | 376 | setValidations({ 377 | checkScopes: false, 378 | checkDuplicates: false, 379 | }); 380 | 381 | const validate = createValidator({ 382 | tests, 383 | }); 384 | 385 | const result = validate('hello world'); 386 | const expected = []; 387 | 388 | t.deepEqual(result, expected); 389 | t.end(); 390 | }); 391 | -------------------------------------------------------------------------------- /packages/formatter-time/lib/time.spec.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import montag from 'montag'; 3 | import chalk from 'chalk'; 4 | import pullout from 'pullout'; 5 | import { 6 | test, 7 | stub, 8 | createTest, 9 | } from 'supertape'; 10 | import * as time from './time.js'; 11 | 12 | const {env} = process; 13 | 14 | const pull = async (stream, i = 9) => { 15 | const output = await pullout(stream); 16 | const SLASH_R = 1; 17 | 18 | return output 19 | .slice(SLASH_R) 20 | .split('\n') 21 | .slice(0, i) 22 | .join('\n'); 23 | }; 24 | 25 | test('supertape: format: time', async (t) => { 26 | const successFn = (t) => { 27 | t.ok(true); 28 | t.end(); 29 | }; 30 | 31 | const successMessage = 'time: success'; 32 | 33 | const failFn = (t) => { 34 | t.ok(false); 35 | t.end(); 36 | }; 37 | 38 | const failMessage = 'time: fail'; 39 | 40 | const {CI} = env; 41 | 42 | env.CI = 1; 43 | 44 | const { 45 | run, 46 | test, 47 | stream, 48 | } = await createTest({ 49 | formatter: time, 50 | }); 51 | 52 | test(successMessage, successFn); 53 | test(failMessage, failFn); 54 | 55 | const [result] = await Promise.all([ 56 | pull(stream, 10), 57 | run(), 58 | ]); 59 | 60 | env.CI = CI; 61 | 62 | const expected = montag` 63 | TAP version 13 64 | 65 | # time: fail 66 | ❌ not ok 2 should be truthy 67 | --- 68 | operator: ok 69 | expected: |- 70 | true 71 | result: |- 72 | false 73 | `; 74 | 75 | t.equal(result, expected); 76 | t.end(); 77 | }); 78 | 79 | test('supertape: format: time: diff', async (t) => { 80 | const fn = (t) => { 81 | t.equal(1, 2); 82 | t.end(); 83 | }; 84 | 85 | const message = 'time'; 86 | 87 | const {CI} = env; 88 | 89 | env.CI = 1; 90 | 91 | const { 92 | run, 93 | test, 94 | stream, 95 | } = await createTest({ 96 | formatter: time, 97 | }); 98 | 99 | test(message, fn); 100 | 101 | const [result] = await Promise.all([ 102 | pull(stream, 9), 103 | run(), 104 | ]); 105 | 106 | env.CI = CI; 107 | 108 | const expected = montag` 109 | TAP version 13 110 | 111 | # time 112 | ❌ not ok 1 should equal 113 | --- 114 | operator: equal 115 | diff: |- 116 | - 2 117 | + 1 118 | `; 119 | 120 | t.equal(result, expected); 121 | t.end(); 122 | }); 123 | 124 | test('supertape: format: time: success', async (t) => { 125 | const fn = (t) => { 126 | t.ok(true); 127 | t.end(); 128 | }; 129 | 130 | const message = 'time: success'; 131 | 132 | env.CI = 1; 133 | 134 | const { 135 | run, 136 | test, 137 | stream, 138 | } = await createTest({ 139 | formatter: time, 140 | }); 141 | 142 | test(message, fn); 143 | 144 | const [result] = await Promise.all([ 145 | pull(stream, 8), 146 | run(), 147 | ]); 148 | 149 | env.CI = 1; 150 | 151 | const expected = montag` 152 | TAP version 13 153 | 154 | 1..1 155 | # tests 1 156 | # pass 1 157 | 158 | # ✅ ok 159 | ` + '\n'; 160 | 161 | t.equal(result, expected); 162 | t.end(); 163 | }); 164 | 165 | test('supertape: format: time: skip', async (t) => { 166 | const fn = (t) => { 167 | t.ok(true); 168 | t.end(); 169 | }; 170 | 171 | const message = 'skip: success'; 172 | 173 | const {CI} = env; 174 | 175 | env.CI = 1; 176 | 177 | const { 178 | run, 179 | test, 180 | stream, 181 | } = await createTest({ 182 | formatter: time, 183 | }); 184 | 185 | test(message, fn, { 186 | skip: true, 187 | }); 188 | 189 | const [result] = await Promise.all([ 190 | pull(stream, 8), 191 | run(), 192 | ]); 193 | 194 | env.CI = CI; 195 | 196 | const expected = montag` 197 | TAP version 13 198 | 199 | 1..0 200 | # tests 0 201 | # pass 0 202 | # ⚠️ skip 1 203 | 204 | # ✅ ok 205 | `; 206 | 207 | t.equal(result, expected); 208 | t.end(); 209 | }); 210 | 211 | test('supertape: format: time: color', async (t) => { 212 | const fn = (t) => { 213 | t.ok(true); 214 | t.end(); 215 | }; 216 | 217 | const message = 'progress-bar: color'; 218 | const {SUPERTAPE_TIME_COLOR} = env; 219 | 220 | updateEnv({ 221 | SUPERTAPE_TIME_COLOR: 'red', 222 | }); 223 | 224 | const { 225 | run, 226 | test, 227 | stream, 228 | } = await createTest({ 229 | formatter: time, 230 | }); 231 | 232 | test(message, fn); 233 | 234 | const [result] = await Promise.all([ 235 | pull(stream, 8), 236 | run(), 237 | ]); 238 | 239 | updateEnv({ 240 | SUPERTAPE_TIME_COLOR, 241 | }); 242 | 243 | const expected = montag` 244 | TAP version 13 245 | 246 | 1..1 247 | # tests 1 248 | # pass 1 249 | 250 | # ✅ ok 251 | ` + '\n'; 252 | 253 | t.equal(result, expected); 254 | t.end(); 255 | }); 256 | 257 | test('supertape: format: time: color: hash', async (t) => { 258 | const fn = (t) => { 259 | t.ok(true); 260 | t.end(); 261 | }; 262 | 263 | const message = 'progress-bar: color'; 264 | const {SUPERTAPE_TIME_COLOR} = env; 265 | 266 | updateEnv({ 267 | SUPERTAPE_TIME_COLOR: 'undefined', 268 | }); 269 | 270 | const { 271 | run, 272 | test, 273 | stream, 274 | } = await createTest({ 275 | formatter: time, 276 | }); 277 | 278 | test(message, fn); 279 | 280 | const [result] = await Promise.all([ 281 | pull(stream, 8), 282 | run(), 283 | ]); 284 | 285 | updateEnv({ 286 | SUPERTAPE_TIME_COLOR, 287 | }); 288 | 289 | const expected = montag` 290 | TAP version 13 291 | 292 | 1..1 293 | # tests 1 294 | # pass 1 295 | 296 | # ✅ ok 297 | ` + '\n'; 298 | 299 | t.equal(result, expected); 300 | t.end(); 301 | }); 302 | 303 | test('supertape: format: time: getStream: no SUPERTAPE_TIME', (t) => { 304 | const {SUPERTAPE_TIME} = env; 305 | 306 | const {_isCI} = global; 307 | 308 | global._isCI = 1; 309 | 310 | updateEnv({ 311 | SUPERTAPE_TIME: '0', 312 | }); 313 | 314 | const stream = time._getStream({ 315 | total: 1, 316 | }); 317 | 318 | updateEnv({ 319 | SUPERTAPE_TIME, 320 | }); 321 | 322 | global._isCI = _isCI; 323 | 324 | t.notEqual(stream, process.stderr); 325 | t.end(); 326 | }); 327 | 328 | test('supertape: format: time: getStream: SUPERTAPE_TIME', (t) => { 329 | const {SUPERTAPE_TIME} = env; 330 | 331 | const {_isCI} = global; 332 | 333 | global._isCI = false; 334 | 335 | updateEnv({ 336 | SUPERTAPE_TIME: '1', 337 | }); 338 | 339 | const stream = time._getStream({ 340 | total: 100, 341 | }); 342 | 343 | updateEnv({ 344 | SUPERTAPE_TIME, 345 | }); 346 | 347 | global._isCI = _isCI; 348 | 349 | t.equal(stream, process.stderr); 350 | t.end(); 351 | }); 352 | 353 | test('supertape: format: time: getStream: SUPERTAPE_TIME, no CI', (t) => { 354 | const {CI, SUPERTAPE_TIME} = env; 355 | 356 | updateEnv({ 357 | SUPERTAPE_TIME: '1', 358 | }); 359 | 360 | delete global._isCI; 361 | 362 | const stream = time._getStream(); 363 | 364 | updateEnv({ 365 | SUPERTAPE_TIME, 366 | }); 367 | 368 | global._isCI = CI; 369 | 370 | t.equal(stream, process.stderr); 371 | t.end(); 372 | }); 373 | 374 | test('supertape: format: time: testEnd', (t) => { 375 | const increment = stub(); 376 | const SingleBar = stub().returns({ 377 | start: stub(), 378 | stop: stub(), 379 | increment, 380 | }); 381 | 382 | const bar = SingleBar(); 383 | 384 | const count = 1; 385 | const total = 10; 386 | const failed = 0; 387 | const test = 'hi'; 388 | 389 | const {testEnd} = time.createFormatter(bar); 390 | 391 | testEnd({ 392 | count, 393 | total, 394 | failed, 395 | test, 396 | }); 397 | 398 | const expected = [{ 399 | count, 400 | total, 401 | failed: '👌', 402 | test, 403 | time: '', 404 | }]; 405 | 406 | t.calledWith(increment, expected); 407 | t.end(); 408 | }); 409 | 410 | test('supertape: format: time: no stack', async (t) => { 411 | const fn = (t) => { 412 | t.ok(false); 413 | t.end(); 414 | }; 415 | 416 | const message = 'progress-bar: success'; 417 | const {CI} = env; 418 | 419 | updateEnv({ 420 | SUPERTAPE_TIME_STACK: '0', 421 | }); 422 | 423 | global._isCI = true; 424 | const { 425 | run, 426 | test, 427 | stream, 428 | } = await createTest({ 429 | formatter: time, 430 | }); 431 | 432 | test(message, fn); 433 | 434 | const [output] = await Promise.all([ 435 | pull(stream, 19), 436 | run(), 437 | ]); 438 | 439 | const result = output.replace(/at .+\n/, 'at xxx\n'); 440 | 441 | delete env.SUPERTAPE_TIME_STACK; 442 | env.CI = CI; 443 | 444 | const expected = montag` 445 | TAP version 13 446 | 447 | # progress-bar: success 448 | ❌ not ok 1 should be truthy 449 | --- 450 | operator: ok 451 | expected: |- 452 | true 453 | result: |- 454 | false 455 | at xxx 456 | ... 457 | 458 | 459 | 1..1 460 | # tests 1 461 | # pass 0 462 | 463 | # ❌ fail 1 464 | `; 465 | 466 | t.equal(result, expected); 467 | t.end(); 468 | }); 469 | 470 | test('formatter: time: maybeZero: no', (t) => { 471 | const result = time.maybeZero(60); 472 | const expected = ''; 473 | 474 | t.equal(result, expected); 475 | t.end(); 476 | }); 477 | 478 | test('formatter: time: maybeZero: yes', (t) => { 479 | const result = time.maybeZero(5); 480 | const expected = '0'; 481 | 482 | t.equal(result, expected); 483 | t.end(); 484 | }); 485 | 486 | test('formatter: time: getColorFn', (t) => { 487 | const result = time.getColorFn('red'); 488 | 489 | t.equal(result, chalk.red); 490 | t.end(); 491 | }); 492 | 493 | function updateEnv(env) { 494 | for (const [name, value] of Object.entries(env)) { 495 | if (value === 'undefined') 496 | delete process.env[name]; 497 | else 498 | process.env[name] = value; 499 | } 500 | } 501 | --------------------------------------------------------------------------------