Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"main": "tspl.js",
"types": "tspl.d.ts",
"scripts": {
"build:types": "npm run delete:types && tsc --project tsconfig.types.json",
"delete:types": "node -e \"import { rmSync } from 'fs'; try {rmSync('./tspl.d.ts'); } catch {} \"",
"lint": "standard",
"lint:fix": "standard --fix",
"test": "npm run test:unit && npm run test:types",
Expand All @@ -22,11 +24,10 @@
"url": "git+https://github.com/mcollina/tspl.git"
},
"license": "MIT",
"dependencies": {
},
"devDependencies": {
"@types/node": "^24.5.2",
"standard": "^17.1.0",
"tsd": "^0.33.0"
"tsd": "^0.33.0",
"typescript": "^5.9.2"
}
}
79 changes: 79 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,60 @@ test('simple test plan', async (t) => {
end()
})

test('simple test plan with passing with providing done to tspl', (t, done) => {
const { strictEqual, end } = tspl(t, { done })
strictEqual(1, 1)
end()
})

test('simple test plan failing and providing done to tspl', () => {
const { strictEqual, end } = tspl({
after: () => {},
strictEqual: assert.strictEqual
}, { plan: 2 })
strictEqual(1, 1)

assert.throws(() => end(), new assert.AssertionError({
message: 'The plan was not completed',
operator: 'strictEqual',
actual: 1,
expected: 2
}))
})

test('simple test plan failing with not providing done to tspl', () => {
const { strictEqual, end } = tspl({
after: () => {},
strictEqual: assert.strictEqual
}, { plan: 2 })
strictEqual(1, 1)

assert.throws(() => end(), new assert.AssertionError({
message: 'The plan was not completed',
operator: 'strictEqual',
actual: 1,
expected: 2
}))
})

test('simple test plan failing with providing done to tspl', () => {
const testDone = (err) => {
assert.deepStrictEqual(err, new assert.AssertionError({
message: 'The plan was not completed',
operator: 'strictEqual',
actual: 1,
expected: 2
}))
}
const { strictEqual, end } = tspl({
after: () => {},
strictEqual: assert.strictEqual
}, { plan: 2, done: testDone })
strictEqual(1, 1)

end()
})

test('simple test plan failing', async (t) => {
let _fn
tspl({
Expand Down Expand Up @@ -93,3 +147,28 @@ test('await plan', async (t) => {

await plan
})

test('t.assert is the same as assert', async (t) => {
t = tspl(t, { plan: 1 })
t.assert.strictEqual(1, 1)
})

test('use t.assert and await plan', async (t) => {
const plan = tspl(t, { plan: 1 })

setImmediate(() => {
plan.assert.strictEqual(1, 1)
})

await plan
})

test('it is possible to override the configure plan by calling .plan()', async (t) => {
t = tspl(t, { plan: 2 })

t.plan(1)

t.assert.strictEqual(1, 1)

await t.completed
})
23 changes: 23 additions & 0 deletions tsconfig.types.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "esnext",
"lib": [
"esnext"
],
"types": [
"node"
],
"declaration": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"allowJs": true,
"emitDeclarationOnly": true,
"skipLibCheck": true,
"checkJs": true,
"removeComments": true
},
"include": [
"tspl.js"
]
}
34 changes: 17 additions & 17 deletions tspl.d.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import test from 'node:test'
import assert from 'node:assert'

type TestFn = Exclude<Parameters<typeof test.test>[0], undefined>
type TextContext = Exclude<Parameters<TestFn>[0], undefined>;

export interface Options {
plan?: number;
export = tspl;
declare function tspl(t: TestContext, opts?: Options): TSPLTestContext;
declare namespace tspl {
export { tspl as default, tspl, Options, TestContext, Assert, TSPLContext, TSPLTestContext, Plan };
}

export type Plan = Omit<typeof assert, 'CallTracker' | 'AssertionError' | 'strict'> & {
completed: Promise<void>
end: () => void
}

export declare function tspl (context: TextContext, opts?: Options): Plan;

export default tspl;
type Options = {
plan?: number;
done?: (err?: Error) => void;
};
type TestContext = import("node:test").TestContext;
type Assert = Omit<typeof assert, "AssertionError" | "CallTracker" | "strict">;
type TSPLContext = {
end: (err?: Error) => void;
completed: Promise<void>;
};
type TSPLTestContext = TestContext & Assert & TSPLContext & PromiseLike<void>;
type Plan = TSPLTestContext;
import assert = require("assert");
91 changes: 76 additions & 15 deletions tspl.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,45 @@

const assert = require('node:assert')

/**
* @typedef {object} TSPLContext
* @property {(err?: Error) => void} end - end the test and check the plan
* @property {Promise<void>} completed - promise that resolves when the plan is completed
*/

/**
* @typedef {object} Options
* @property {number} [plan] - number of expected assertions
* @property {(err?: Error) => void} [done] - callback when the plan is completed
*/

/** @typedef {import('node:test').TestContext} TestContext */
/** @typedef {Omit<import('node:assert'), 'AssertionError' | 'CallTracker' | 'strict'>} AssertFns */
/** @typedef {TestContext & AssertFns & TSPLContext & PromiseLike<void>} TSPLTestContext */
/** @typedef {TSPLTestContext} Plan */

/**
* @param {TestContext} t
* @param {Options} [opts={}]
* @returns {TSPLTestContext}
*/
function tspl (t, opts = {}) {
if (t) {
t.after(autoEnd)
}

let ended = false
const { plan } = opts
let {
plan,
done
} = opts
let actual = 0

t.plan = function (n) {
plan = n
return t
}

let _resolve
const completed = new Promise((resolve) => {
_resolve = resolve
Expand All @@ -35,38 +65,69 @@ function tspl (t, opts = {}) {
ended = true

if (plan) {
assert.strictEqual(actual, plan, t.name ? `The plan for '${t.name}' was not completed` : 'The plan was not completed')
try {
assert.strictEqual(actual, plan, t.name ? `The plan for '${t.name}' was not completed` : 'The plan was not completed')
_resolve()
done && done()
} catch (err) {
_resolve()
if (done) {
done(err)
} else {
throw err
}
}
} else {
done && done()
_resolve()
}
}

const res = {
completed,
end
}

Object.defineProperty(res, 'then', {
get () {
return completed.then.bind(completed)
Object.defineProperties(t, {
then: {
get () {
return completed.then.bind(completed)
}
},
end: {
get () {
return end
}
},
completed: {
get () {
return completed
}
}
})

const assertTestContext = {}

for (const method of Object.keys(assert)) {
if (method.match(/^[a-z]/)) {
res[method] = (...args) => {
if (/^[a-z]/.test(method) && typeof assert[method] === 'function' && method !== 'strict') {
const fn = (...args) => {
actual++
const res = assert[method](...args)
const result = assert[method](...args)

if (actual === plan) {
_resolve()
}

return res
return result
}
assertTestContext[method] = fn
}
}

return res
Object.assign(t, assertTestContext)

Object.defineProperty(t, 'assert', {
get () {
return assertTestContext
}
})

return /** @type {TSPLTestContext} */ (/** @type {*} */(t))
}

module.exports = tspl
Expand Down
22 changes: 22 additions & 0 deletions tspl.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,25 @@ test('tspl', (t) => {

expectType<void>(p.end());
});

test('tspl', (t) => {
const p: Plan = tspl(t);

p.assert.strictEqual(1, 1);

expectType<void>(p.end());
});

test('tspl', async (t) => {
const p: Plan = tspl(t);

p.assert.strictEqual(1, 1);

await p
});

test('tspl', (t, done) => {
const p: Plan = tspl(t, { done });

p.assert.strictEqual(1, 1);
});