diff --git a/README.md b/README.md index 4093734..bcc783d 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ const warning = createWarning({ message: 'Hello %s', unlimited: true }) -warning('world') +const emitted = warning('world') ``` #### Methods @@ -101,6 +101,45 @@ FST_ERROR_CODE('world') // will be emitted FST_ERROR_CODE('world') // will be emitted again ``` +#### `spyWarning(warning)` + +Spy the created warning function for testing purpose. + +```js +const { test } = require('node:test') +const { createWarning, spyWarning } = require('process-warning') +const FST_ERROR_CODE = createWarning({ name: 'MyAppWarning', code: 'FST_ERROR_CODE', message: 'Hello %s' }) + +test('spy warning', t => { + const spyData = spyWarning(FST_ERROR_CODE) + + // call after spy + const emitted = FST_ERROR_CODE('world') + t.assert.strictEqual(emitted, true) + + // restore the warning function + // it must be called when you do not need to spy anymore + // otherwise, the calls data will accumulates. + t.after(() => spyData.restore()) + + t.assert.strictEqual(FST_ERROR_CODE.emitted, true) + // calls return the arguments and result. + // result indicate whether warning is emitted through process.emitWarning + console.log(spyData.calls) // [{ arguments: ['world'], result: true }] + t.assert.deepStrictEqual(spyData.calls[0].arguments, ['world']) + t.assert.strictEqual(spyData.calls[0].result, true) + // number of times called the function + console.log(spyData.callCount()) // 1 + t.assert.strictEqual(spyData.callCount(), 1) + + // reset the spy stat and warning state + spyData.reset() + t.assert.strictEqual(FST_ERROR_CODE.emitted, false) + t.assert.deepStrictEqual(spyData.calls, []) + t.assert.strictEqual(spyData.callCount(), 0) +}) +``` + #### Suppressing warnings It is possible to suppress warnings by utilizing one of node's built-in warning suppression mechanisms. diff --git a/index.js b/index.js index e0d4ab8..78455f8 100644 --- a/index.js +++ b/index.js @@ -17,6 +17,7 @@ const { format } = require('node:util') * @property {string} message - The warning message. * @property {boolean} emitted - Indicates if the warning has been emitted. * @property {function} format - Formats the warning message. + * @returns {boolean} emit - Indicates if the warning has been emitted by this call. */ /** @@ -33,8 +34,69 @@ const { format } = require('node:util') * @typedef {Object} ProcessWarning * @property {function} createWarning - Creates a warning item. * @property {function} createDeprecation - Creates a deprecation warning item. + * @property {function} spyWarning - Spy a warning item. */ +/** + * Represents the spy data. + * @typedef {Object} WarningSpyData + * @property {object} calls - Arguments of WarningItem calls with. + * @property {function} callCount - Number of counts called the WarningItem. + * @property {function} reset - Reset the calls data and state of WarningItem. + * @property {function} restore - Remove spy from WarningItem. + */ + +const kWarningFn = Symbol('process-warning.fn') +const kWarningSpyData = Symbol('process-warning.spyData') + +/** + * Spy a warning item. + * @function + * @memberof processWarning + * @param {WarningItem} warning - The warning item to spy. + * @returns {WarningSpyData} The created spy data. + */ +function spyWarning (warning) { + // Do not double spy the same warning + if (warning[kWarningSpyData] === null) { + const warningFn = warning[kWarningFn] + warning[kWarningFn] = function (a, b, c) { + const args = [] + // since warning always call by fn(a, b, c) + // it need to remove the trailing undefined arguments + if (c) { + args.push(a, b, c) + } else if (b) { + args.push(a, b) + } else if (a) { + args.push(a) + } + warning[kWarningSpyData].calls.push({ + arguments: args, + result: warningFn(a, b, c) + }) + } + const spyData = { + calls: [], + callCount () { + return spyData.calls.length + }, + reset () { + warning.emitted = false + spyData.calls.length = 0 + }, + restore () { + spyData.reset() + warning[kWarningFn] = warningFn + warning[kWarningSpyData] = null + } + } + warning[kWarningSpyData] = spyData + } + + return warning[kWarningSpyData] +} + /** * Creates a deprecation warning item. * @function @@ -62,21 +124,24 @@ function createWarning ({ name, code, message, unlimited = false } = {}) { code = code.toUpperCase() - let warningContainer = { - [name]: function (a, b, c) { + const warningFn = unlimited === true + ? function (a, b, c) { + warning.emitted = true + process.emitWarning(warning.format(a, b, c), warning.name, warning.code) + return true + } + : function (a, b, c) { if (warning.emitted === true && warning.unlimited !== true) { - return + return false } warning.emitted = true process.emitWarning(warning.format(a, b, c), warning.name, warning.code) + return true } - } - if (unlimited) { - warningContainer = { - [name]: function (a, b, c) { - warning.emitted = true - process.emitWarning(warning.format(a, b, c), warning.name, warning.code) - } + + const warningContainer = { + [name]: function (a, b, c) { + return warning[kWarningFn](a, b, c) } } @@ -86,14 +151,16 @@ function createWarning ({ name, code, message, unlimited = false } = {}) { warning.message = message warning.unlimited = unlimited warning.code = code + warning[kWarningFn] = warningFn + warning[kWarningSpyData] = null /** - * Formats the warning message. - * @param {*} [a] Possible message interpolation value. - * @param {*} [b] Possible message interpolation value. - * @param {*} [c] Possible message interpolation value. - * @returns {string} The formatted warning message. - */ + * Formats the warning message. + * @param {*} [a] Possible message interpolation value. + * @param {*} [b] Possible message interpolation value. + * @param {*} [c] Possible message interpolation value. + * @returns {string} The formatted warning message. + */ warning.format = function (a, b, c) { let formatted if (a && b && c) { @@ -116,9 +183,10 @@ function createWarning ({ name, code, message, unlimited = false } = {}) { * @namespace * @property {function} createWarning - Creates a warning item. * @property {function} createDeprecation - Creates a deprecation warning item. + * @property {function} spyWarning - Spy a warning item. * @property {ProcessWarning} processWarning - Represents the process warning functionality. */ -const out = { createWarning, createDeprecation } +const out = { createWarning, createDeprecation, spyWarning } module.exports = out module.exports.default = out module.exports.processWarning = out diff --git a/test/emit-once-only.test.js b/test/emit-once-only.test.js index 4d5bc1f..cf9818c 100644 --- a/test/emit-once-only.test.js +++ b/test/emit-once-only.test.js @@ -5,7 +5,7 @@ const { createWarning } = require('..') const { withResolvers } = require('./promise') test('emit should emit a given code only once', t => { - t.plan(4) + t.plan(6) const { promise, resolve } = withResolvers() @@ -22,8 +22,8 @@ test('emit should emit a given code only once', t => { code: 'CODE', message: 'Hello world' }) - warn() - warn() + t.assert.strictEqual(warn(), true) + t.assert.strictEqual(warn(), false) setImmediate(() => { process.removeListener('warning', onWarning) resolve() diff --git a/test/emit-reset.test.js b/test/emit-reset.test.js index 1a31a4c..c5e6216 100644 --- a/test/emit-reset.test.js +++ b/test/emit-reset.test.js @@ -5,7 +5,7 @@ const { createWarning } = require('../') const { withResolvers } = require('./promise') test('a limited warning can be re-set', t => { - t.plan(4) + t.plan(7) const { promise, resolve } = withResolvers() let count = 0 @@ -20,14 +20,14 @@ test('a limited warning can be re-set', t => { message: 'Hello world' }) - warn() + t.assert.strictEqual(warn(), true) t.assert.ok(warn.emitted) - warn() + t.assert.strictEqual(warn(), false) t.assert.ok(warn.emitted) warn.emitted = false - warn() + t.assert.strictEqual(warn(), true) t.assert.ok(warn.emitted) setImmediate(() => { diff --git a/test/emit-unlimited.test.js b/test/emit-unlimited.test.js index 3bf4780..2511e92 100644 --- a/test/emit-unlimited.test.js +++ b/test/emit-unlimited.test.js @@ -5,7 +5,7 @@ const { createWarning } = require('..') const { withResolvers } = require('./promise') test('emit should emit a given code unlimited times', t => { - t.plan(50) + t.plan(60) let runs = 0 const expectedRun = [] @@ -31,7 +31,7 @@ test('emit should emit a given code unlimited times', t => { for (let i = 0; i < times; i++) { expectedRun.push(i) - warn() + t.assert.strictEqual(warn(), true) } setImmediate(() => { process.removeListener('warning', onWarning) diff --git a/test/spy-warning.test.js b/test/spy-warning.test.js new file mode 100644 index 0000000..9634a98 --- /dev/null +++ b/test/spy-warning.test.js @@ -0,0 +1,143 @@ +'use strict' + +const { test } = require('node:test') +const { createWarning } = require('..') +const { spyWarning } = require('..') + +test('Spy ProcessWarning - unlimited: false', t => { + const warning = createWarning({ + name: 'Warning', + code: 'WRN', + message: 'Hello %s' + }) + const spyData = spyWarning(warning) + + t.assert.strictEqual(warning.emitted, false) + t.assert.deepStrictEqual(spyData.calls, []) + t.assert.strictEqual(spyData.callCount(), 0) + + warning('World') + t.assert.strictEqual(warning.emitted, true) + t.assert.deepStrictEqual(spyData.calls[0].arguments, ['World']) + t.assert.strictEqual(spyData.calls[0].result, true) + t.assert.strictEqual(spyData.callCount(), 1) + + warning('World') + t.assert.strictEqual(warning.emitted, true) + t.assert.deepStrictEqual(spyData.calls[0].arguments, ['World']) + t.assert.strictEqual(spyData.calls[0].result, true) + t.assert.deepStrictEqual(spyData.calls[1].arguments, ['World']) + t.assert.strictEqual(spyData.calls[1].result, false) + t.assert.strictEqual(spyData.callCount(), 2) + + spyData.reset() + t.assert.strictEqual(warning.emitted, false) + t.assert.deepStrictEqual(spyData.calls, []) + t.assert.strictEqual(spyData.callCount(), 0) + + warning('World') + t.assert.strictEqual(warning.emitted, true) + t.assert.deepStrictEqual(spyData.calls[0].arguments, ['World']) + t.assert.strictEqual(spyData.calls[0].result, true) + t.assert.strictEqual(spyData.callCount(), 1) + + spyData.restore() + t.assert.strictEqual(warning.emitted, false) + t.assert.deepStrictEqual(spyData.calls, []) + t.assert.strictEqual(spyData.callCount(), 0) + + warning('World') + t.assert.strictEqual(warning.emitted, true) + t.assert.deepStrictEqual(spyData.calls, []) + t.assert.strictEqual(spyData.callCount(), 0) +}) + +test('Spy ProcessWarning - unlimited: true', t => { + const warning = createWarning({ + name: 'Warning', + code: 'WRN', + message: 'Hello %s', + unlimited: true + }) + const spyData = spyWarning(warning) + + t.assert.strictEqual(warning.emitted, false) + t.assert.deepStrictEqual(spyData.calls, []) + t.assert.strictEqual(spyData.callCount(), 0) + + warning('World') + t.assert.strictEqual(warning.emitted, true) + t.assert.deepStrictEqual(spyData.calls[0].arguments, ['World']) + t.assert.strictEqual(spyData.calls[0].result, true) + t.assert.strictEqual(spyData.callCount(), 1) + + warning('World') + t.assert.strictEqual(warning.emitted, true) + t.assert.deepStrictEqual(spyData.calls[0].arguments, ['World']) + t.assert.strictEqual(spyData.calls[0].result, true) + t.assert.deepStrictEqual(spyData.calls[1].arguments, ['World']) + t.assert.strictEqual(spyData.calls[1].result, true) + t.assert.strictEqual(spyData.callCount(), 2) + + spyData.reset() + t.assert.strictEqual(warning.emitted, false) + t.assert.deepStrictEqual(spyData.calls, []) + t.assert.strictEqual(spyData.callCount(), 0) + + warning('World') + t.assert.strictEqual(warning.emitted, true) + t.assert.deepStrictEqual(spyData.calls[0].arguments, ['World']) + t.assert.strictEqual(spyData.calls[0].result, true) + t.assert.strictEqual(spyData.callCount(), 1) + + spyData.restore() + t.assert.strictEqual(warning.emitted, false) + t.assert.deepStrictEqual(spyData.calls, []) + t.assert.strictEqual(spyData.callCount(), 0) + + warning('World') + t.assert.strictEqual(warning.emitted, true) + t.assert.deepStrictEqual(spyData.calls, []) + t.assert.strictEqual(spyData.callCount(), 0) +}) + +test('Spy ProcessWarning - calls[].arguments', t => { + const warning = createWarning({ + name: 'Warning', + code: 'WRN', + message: 'Hello %s' + }) + const spyData = spyWarning(warning) + + warning(undefined) + t.assert.deepStrictEqual(spyData.calls[0].arguments, []) + spyData.reset() + + warning() + t.assert.deepStrictEqual(spyData.calls[0].arguments, []) + spyData.reset() + + warning('World') + t.assert.deepStrictEqual(spyData.calls[0].arguments, ['World']) + spyData.reset() + + warning('World', 'Hello') + t.assert.deepStrictEqual(spyData.calls[0].arguments, ['World', 'Hello']) + spyData.reset() + + warning(undefined, 'Hello') + t.assert.deepStrictEqual(spyData.calls[0].arguments, [undefined, 'Hello']) + spyData.reset() + + warning('World', 'Hello', 'World') + t.assert.deepStrictEqual(spyData.calls[0].arguments, ['World', 'Hello', 'World']) + spyData.reset() + + warning('World', undefined, 'World') + t.assert.deepStrictEqual(spyData.calls[0].arguments, ['World', undefined, 'World']) + spyData.reset() + + warning(undefined, 'Hello', 'World') + t.assert.deepStrictEqual(spyData.calls[0].arguments, [undefined, 'Hello', 'World']) + spyData.reset() +}) diff --git a/types/index.d.ts b/types/index.d.ts index 0728405..2ecbb29 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,6 +1,6 @@ declare namespace processWarning { export interface WarningItem { - (a?: any, b?: any, c?: any): void; + (a?: any, b?: any, c?: any): boolean; name: string; code: string; message: string; @@ -22,13 +22,27 @@ declare namespace processWarning { unlimited?: boolean; } + export type WarningSpyData = { + calls: WarningCallData[], + callCount(): number + reset(): void + restore(): void + } + + export type WarningCallData = { + arguments: unknown[] + result: boolean + } + export type ProcessWarning = { createWarning(params: WarningOptions): WarningItem; createDeprecation(params: DeprecationOptions): WarningItem; + spyWarning(warning: WarningItem): WarningSpyData } export function createWarning (params: WarningOptions): WarningItem export function createDeprecation (params: DeprecationOptions): WarningItem + export function spyWarning (warning: WarningItem): WarningSpyData const processWarning: ProcessWarning export { processWarning as default } diff --git a/types/index.tst.ts b/types/index.tst.ts index 2f953af..2de4687 100644 --- a/types/index.tst.ts +++ b/types/index.tst.ts @@ -1,5 +1,5 @@ import { expect } from 'tstyche' -import { createWarning, createDeprecation } from '..' +import { createDeprecation, createWarning, spyWarning, WarningCallData, WarningSpyData } from '..' const WarnInstance = createWarning({ name: 'TypeScriptWarning', @@ -13,9 +13,9 @@ expect(WarnInstance.name).type.toBe() expect(WarnInstance.emitted).type.toBe() expect(WarnInstance.unlimited).type.toBe() -expect(WarnInstance()).type.toBe() -expect(WarnInstance('foo')).type.toBe() -expect(WarnInstance('foo', 'bar')).type.toBe() +expect(WarnInstance()).type.toBe() +expect(WarnInstance('foo')).type.toBe() +expect(WarnInstance('foo', 'bar')).type.toBe() const buildWarnUnlimited = createWarning({ name: 'TypeScriptWarning', @@ -33,6 +33,16 @@ const DeprecationInstance = createDeprecation({ expect(DeprecationInstance.code).type.toBe() -expect(DeprecationInstance()).type.toBe() -expect(DeprecationInstance('foo')).type.toBe() -expect(DeprecationInstance('foo', 'bar')).type.toBe() +expect(DeprecationInstance()).type.toBe() +expect(DeprecationInstance('foo')).type.toBe() +expect(DeprecationInstance('foo', 'bar')).type.toBe() + +const spyData = spyWarning(WarnInstance) +expect(spyData).type.toBe() +expect(spyData.calls).type.toBe() +expect(spyData.callCount).type.toBe<(() => number)>() +expect(spyData.callCount()).type.toBe() +expect(spyData.reset).type.toBe<(() => void)>() +expect(spyData.reset()).type.toBe() +expect(spyData.restore).type.toBe<(() => void)>() +expect(spyData.restore()).type.toBe()