├── .npmrc ├── demo.js ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── tspl.d.ts ├── tspl.test-d.ts ├── package.json ├── LICENSE ├── tspl.js ├── README.md ├── test.js └── .gitignore /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /demo.js: -------------------------------------------------------------------------------- 1 | const test = require('node:test') 2 | const tspl = require('.') 3 | 4 | test('tspl', async (t) => { 5 | const { strictEqual } = tspl(t, { plan: 2 }) 6 | strictEqual(1, 1) 7 | }) 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: standard 10 | versions: 11 | - 16.0.3 12 | -------------------------------------------------------------------------------- /tspl.d.ts: -------------------------------------------------------------------------------- 1 | import test from 'node:test' 2 | import assert from 'node:assert' 3 | 4 | type TestFn = Exclude[0], undefined> 5 | type TextContext = Exclude[0], undefined>; 6 | 7 | export interface Options { 8 | plan?: number; 9 | } 10 | 11 | export type Plan = Omit & { 12 | completed: Promise 13 | end: () => void 14 | } 15 | 16 | export declare function tspl (context: TextContext, opts?: Options): Plan; 17 | 18 | export default tspl; 19 | -------------------------------------------------------------------------------- /tspl.test-d.ts: -------------------------------------------------------------------------------- 1 | import test from 'node:test'; 2 | import { expectType } from 'tsd'; 3 | import { tspl, Plan } from './tspl'; 4 | 5 | test('tspl', (t) => { 6 | expectType(tspl(t, { plan: 1 })); 7 | const p: Plan = tspl(t, { plan: 1 }); 8 | 9 | p.strictEqual(1, 1); 10 | 11 | expectType(p.end()); 12 | }); 13 | 14 | test('completed', async (t) => { 15 | expectType(tspl(t, { plan: 1 })); 16 | const p: Plan = tspl(t, { plan: 1 }); 17 | 18 | setTimeout(() => { 19 | p.strictEqual(1, 1); 20 | }); 21 | 22 | await p.completed 23 | }); 24 | 25 | test('tspl', (t) => { 26 | const p: Plan = tspl(t); 27 | 28 | p.strictEqual(1, 1); 29 | 30 | expectType(p.end()); 31 | }); 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@matteo.collina/tspl", 3 | "version": "0.2.0", 4 | "description": "Count the number of assertions for node:test", 5 | "main": "tspl.js", 6 | "types": "tspl.d.ts", 7 | "scripts": { 8 | "lint": "standard", 9 | "lint:fix": "standard --fix", 10 | "test": "npm run test:unit && npm run test:types", 11 | "test:unit": "node --test test.js && tsd", 12 | "test:types": "tsd" 13 | }, 14 | "keywords": [ 15 | "node:test", 16 | "plan", 17 | "assert" 18 | ], 19 | "author": "Matteo Collina ", 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/mcollina/tspl.git" 23 | }, 24 | "license": "MIT", 25 | "dependencies": { 26 | }, 27 | "devDependencies": { 28 | "@types/node": "^24.5.2", 29 | "standard": "^17.1.0", 30 | "tsd": "^0.33.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Matteo Collina 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 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout repository 11 | uses: actions/checkout@v5 12 | with: 13 | persist-credentials: false 14 | 15 | - name: Install Node.js 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: '24' 19 | 20 | - name: Install dependencies 21 | run: npm install 22 | 23 | - name: Run linter 24 | run: npm run lint 25 | 26 | 27 | typescript: 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - name: Checkout repository 32 | uses: actions/checkout@v5 33 | with: 34 | persist-credentials: false 35 | 36 | - name: Install Node.js 37 | uses: actions/setup-node@v1 38 | with: 39 | node-version: '24' 40 | 41 | - name: Install dependencies 42 | run: npm install 43 | 44 | - name: Check types 45 | run: npm run test:types 46 | 47 | test: 48 | runs-on: ubuntu-latest 49 | 50 | strategy: 51 | matrix: 52 | node-version: ['18', '20', '22', '24'] 53 | 54 | steps: 55 | - name: Checkout repository 56 | uses: actions/checkout@v5 57 | with: 58 | persist-credentials: false 59 | 60 | - name: Install Node.js ${{ matrix.node-version }} 61 | uses: actions/setup-node@v1 62 | with: 63 | node-version: ${{ matrix.node-version }} 64 | 65 | - name: Install 66 | run: | 67 | npm install 68 | 69 | - name: Run tests 70 | run: | 71 | npm run test:unit 72 | -------------------------------------------------------------------------------- /tspl.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('node:assert') 4 | 5 | function tspl (t, opts = {}) { 6 | if (t) { 7 | t.after(autoEnd) 8 | } 9 | 10 | let ended = false 11 | const { plan } = opts 12 | let actual = 0 13 | 14 | let _resolve 15 | const completed = new Promise((resolve) => { 16 | _resolve = resolve 17 | }) 18 | 19 | async function autoEnd () { 20 | if (ended) { 21 | return 22 | } 23 | 24 | if (plan) { 25 | assert.strictEqual(actual, plan, t.name ? `The plan for '${t.name}' was not completed` : 'The plan was not completed') 26 | } else { 27 | assert.fail(t.name ? `The plan for '${t.name}' was not completed` : 'The plan was not completed') 28 | } 29 | } 30 | 31 | function end () { 32 | if (ended) { 33 | return 34 | } 35 | ended = true 36 | 37 | if (plan) { 38 | assert.strictEqual(actual, plan, t.name ? `The plan for '${t.name}' was not completed` : 'The plan was not completed') 39 | _resolve() 40 | } 41 | } 42 | 43 | const res = { 44 | completed, 45 | end 46 | } 47 | 48 | Object.defineProperty(res, 'then', { 49 | get () { 50 | return completed.then.bind(completed) 51 | } 52 | }) 53 | 54 | for (const method of Object.keys(assert)) { 55 | if (method.match(/^[a-z]/)) { 56 | res[method] = (...args) => { 57 | actual++ 58 | const res = assert[method](...args) 59 | 60 | if (actual === plan) { 61 | _resolve() 62 | } 63 | 64 | return res 65 | } 66 | } 67 | } 68 | 69 | return res 70 | } 71 | 72 | module.exports = tspl 73 | module.exports.default = tspl 74 | module.exports.tspl = tspl 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tspl 2 | 3 | Test Planner for [`node:test`](https://nodejs.org/api/test.html) 4 | and [`node:assert`](https://nodejs.org/api/assert.html). 5 | It fails your tests if the number of assertions is not met, 6 | or the test plan was not completed. 7 | 8 | > [!TIP] 9 | > Since [Node 20.15.0](https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V20.md#20.15.0) / 10 | > [Node 22.2.0](https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V22.md#22.2.0) 11 | > this functionality is baked into `node:test` itself (see 12 | > [`context.plan(count)`](https://nodejs.org/api/test.html#contextplancountoptions)). 13 | > If you only use those versions or later, then you do not 14 | > need this module. 15 | 16 | ## Installation 17 | 18 | ```bash 19 | npm i @matteo.collina/tspl --save-dev 20 | ``` 21 | 22 | (You'll also need `@types/node`) 23 | 24 | ## Example 25 | 26 | ```js 27 | import test from 'node:test'; 28 | import { tspl } from '@matteo.collina/tspl'; 29 | 30 | test('tspl', (t) => { 31 | const { strictEqual } = tspl(t, { plan: 1 }); 32 | strictEqual(1, 1); 33 | }); 34 | ``` 35 | 36 | ### Typescript 37 | 38 | ```typescript 39 | import test from 'node:test'; 40 | import { tspl, type Plan } from '@matteo.collina/tspl'; 41 | 42 | test('tspl', (t) => { 43 | const p: Plan = tspl(t, { plan: 1 }); 44 | p.strictEqual(1, 1); 45 | }); 46 | ``` 47 | 48 | ## API 49 | 50 | ### __`tspl(t: TestContext, options): Plan`__ 51 | 52 | Create a plan for the current test. 53 | 54 | Here are the options: 55 | 56 | * `plan`: how many assertions are planned 57 | 58 | ### `Plan` 59 | 60 | The plan includes all exports from [`node:assert`](https://nodejs.org/api/assert.html), 61 | as well as: 62 | 63 | * `end()`: a function to complete the plan 64 | * `completed`: a promise that will resolve when the plan is completed. 65 | 66 | ## License 67 | 68 | MIT 69 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('node:test') 4 | const assert = require('node:assert') 5 | const tspl = require('.') 6 | 7 | test('simple test plan', async (t) => { 8 | const { strictEqual, end } = tspl(t) 9 | strictEqual(1, 1) 10 | end() 11 | }) 12 | 13 | test('simple test plan failing', async (t) => { 14 | let _fn 15 | tspl({ 16 | after (fn) { 17 | _fn = fn 18 | } 19 | }) 20 | await assert.rejects(_fn, new assert.AssertionError({ 21 | message: 'The plan was not completed', 22 | operator: 'fail' 23 | })) 24 | }) 25 | 26 | test('simple test plan with counter', async (t) => { 27 | const { strictEqual, end } = tspl(t, { plan: 2 }) 28 | strictEqual(1, 1) 29 | strictEqual(1, 1) 30 | end() 31 | }) 32 | 33 | test('simple test plan with counter failing', async (t) => { 34 | const { end } = tspl(t, { plan: 2 }) 35 | assert.throws(end, new assert.AssertionError({ 36 | message: 'The plan for \'simple test plan with counter failing\' was not completed', 37 | operator: 'strictEqual', 38 | expected: 2, 39 | actual: 0 40 | })) 41 | }) 42 | 43 | test('counter failing in after', async (t) => { 44 | let _fn 45 | tspl({ 46 | after (fn) { 47 | _fn = fn 48 | } 49 | }, { 50 | plan: 2 51 | }) 52 | await assert.rejects(_fn, new assert.AssertionError({ 53 | message: 'The plan was not completed', 54 | operator: 'strictEqual', 55 | expected: 2, 56 | actual: 0 57 | })) 58 | }) 59 | 60 | test('counter completed in after', async (t) => { 61 | let _fn 62 | const { strictEqual } = tspl({ 63 | after (fn) { 64 | _fn = fn 65 | } 66 | }, { 67 | plan: 2 68 | }) 69 | 70 | strictEqual(1, 1) 71 | strictEqual(1, 1) 72 | 73 | // no errors 74 | await _fn() 75 | }) 76 | 77 | test('wait', async (t) => { 78 | const { strictEqual, completed } = tspl(t, { plan: 1 }) 79 | 80 | setImmediate(() => { 81 | strictEqual(1, 1) 82 | }) 83 | 84 | await completed 85 | }) 86 | 87 | test('await plan', async (t) => { 88 | const plan = tspl(t, { plan: 1 }) 89 | 90 | setImmediate(() => { 91 | plan.strictEqual(1, 1) 92 | }) 93 | 94 | await plan 95 | }) 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | --------------------------------------------------------------------------------