diff --git a/package.json b/package.json index 79ecf727..100de4c9 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "test:deno": "FILTER='deno-' npm run test:ci", "test:bun": "FILTER='bun-' npm run test:ci", "prebuild": "rm -rf ./lib ./ci", + "predocker:deno": "docker compose -f ./test/docker/playground/deno/docker-compose.yml down", + "docker:deno": "docker compose -f ./test/docker/playground/deno/docker-compose.yml up --build", "build": "npx tsc; npx tsc -p tsconfig.test.json", "postbuild": "npx tsx ./tools/compatibility/node.ts; chmod +x lib/bin/index.js; npm audit", "eslint:checker": "npx eslint . --ext .js,.ts", diff --git a/src/configs/each.ts b/src/configs/each.ts new file mode 100644 index 00000000..cf5d93ea --- /dev/null +++ b/src/configs/each.ts @@ -0,0 +1,24 @@ +export type Control = { + pause: () => void; + continue: () => void; + reset: () => void; +}; + +export type EachConfigs = { + status: boolean; + cb?: () => unknown | Promise; +}; + +export const each: { + before: EachConfigs; + after: EachConfigs; +} = { + before: { + status: true, + cb: undefined, + }, + after: { + status: true, + cb: undefined, + }, +}; diff --git a/src/helpers/find-file.ts b/src/helpers/find-file.ts new file mode 100644 index 00000000..133ee395 --- /dev/null +++ b/src/helpers/find-file.ts @@ -0,0 +1,31 @@ +import { EOL } from 'node:os'; + +export const findFile = (error: Error) => { + const stackLines = error.stack?.split(EOL) || []; + + let file = ''; + + const basePath = 'poku/lib/'; + + for (const line of stackLines) { + if (!line.includes(basePath)) { + const match = line.match( + /at\s(\/.+|file:.+)|^(\s+)at\smodule\scode\s\((\/.+|file:.+)\)/i + ); + + // Node and Deno + if (match && match[1]) { + file = match[1]; + break; + } + + // Bun + if (match && match[3]) { + file = match[3]; + break; + } + } + } + + return file; +}; diff --git a/src/helpers/format.ts b/src/helpers/format.ts index 75063a5c..fc8ecc22 100644 --- a/src/helpers/format.ts +++ b/src/helpers/format.ts @@ -1,5 +1,23 @@ import { padStart } from './pad.js'; +export const backgroundColor = { + white: 7, + black: 40, + grey: 100, + red: 41, + green: 42, + yellow: 43, + blue: 44, + magenta: 45, + cyan: 46, + brightRed: 101, + brightGreen: 102, + brightYellow: 103, + brightBlue: 104, + brightMagenta: 105, + brightCyan: 106, +} as const; + export const format = { counter: (current: number, total: number, pad = '0') => { const totalDigits = String(total).length; diff --git a/src/helpers/parseAsssetion.ts b/src/helpers/parseAsssetion.ts index a927e414..68f7a02c 100644 --- a/src/helpers/parseAsssetion.ts +++ b/src/helpers/parseAsssetion.ts @@ -4,6 +4,8 @@ import assert from 'node:assert'; import { EOL } from 'node:os'; import { format } from './format.js'; import { hr } from './hr.js'; +import { findFile } from './find-file.js'; +import { each } from '../configs/each.js'; export type ParseAssertionOptions = { message?: string | Error; @@ -14,40 +16,8 @@ export type ParseAssertionOptions = { hideDiff?: boolean; }; -const findFile = (error: Error) => { - const stackLines = error.stack?.split(EOL) || []; - - let file = ''; - - const basePath = 'poku/lib/'; - - for (const line of stackLines) { - if (!line.includes(basePath)) { - const match = line.match( - /at\s(\/.+|file:.+)|^(\s+)at\smodule\scode\s\((\/.+|file:.+)\)/i - ); - - // Node and Deno - if (match && match[1]) { - file = match[1]; - break; - } - - // Bun - if (match && match[3]) { - file = match[3]; - break; - } - } - } - - return file; -}; - -const formatFail = (str: string) => format.bold(format.fail(`✘ ${str}`)); - -export const parseAssertion = ( - cb: () => void, +export const parseAssertion = async ( + cb: () => void | Promise, options: ParseAssertionOptions ) => { const isPoku = @@ -55,7 +25,18 @@ export const parseAssertion = ( const FILE = process.env.FILE; try { - cb(); + if (typeof each.before.cb === 'function') { + const beforeResult = each.before.cb(); + if (beforeResult instanceof Promise) await beforeResult; + } + + const cbResult = cb(); + if (cbResult instanceof Promise) await cbResult; + + if (typeof each.after.cb === 'function') { + const afterResult = each.after.cb(); + if (afterResult instanceof Promise) await afterResult; + } if (typeof options.message === 'string') { const message = isPoku @@ -67,7 +48,7 @@ export const parseAssertion = ( } catch (error) { if (error instanceof assert.AssertionError) { const { code, actual, expected, operator } = error; - const absoultePath = findFile(error).replace(/file:/, ''); + const absoultePath = findFile(error).replace(/file:(\/\/)?/, ''); const file = path.relative(path.resolve(process.cwd()), absoultePath); let message: string = ''; @@ -80,8 +61,8 @@ export const parseAssertion = ( const finalMessage = message?.trim().length > 0 - ? `${formatFail(message)}` - : `${formatFail('No Message')}`; + ? format.bold(format.fail(`✘ ${message}`)) + : format.bold(format.fail('✘ No Message')); console.log( isPoku diff --git a/src/index.ts b/src/index.ts index 7d876431..0c0f4dc6 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ export { poku } from './modules/poku.js'; export { exit } from './modules/exit.js'; export { assert } from './modules/assert.js'; +export { describe, log } from './modules/describe.js'; export { assertPromise } from './modules/assert-promise.js'; export { beforeEach, afterEach } from './modules/each.js'; export { publicListFiles as listFiles } from './modules/list-files.js'; diff --git a/src/modules/assert-promise.ts b/src/modules/assert-promise.ts index 6ee1ab2b..fcb71719 100644 --- a/src/modules/assert-promise.ts +++ b/src/modules/assert-promise.ts @@ -1,17 +1,27 @@ +import process from 'node:process'; import * as nodeAssert from 'node:assert'; import { parseAssertion, ParseAssertionOptions, } from '../helpers/parseAsssetion.js'; -import { each } from './assert.js'; +import { getRuntime } from '../helpers/get-runtime.js'; + +const runtime = getRuntime(); +const version = + runtime === 'node' + ? Number(process.version.match(/v(\d+)\./)?.[1]) + : undefined; const ok = async ( value: unknown, message?: ParseAssertionOptions['message'] ): Promise => { - if (each.before) await each.before(); - parseAssertion(() => nodeAssert.ok(value), { message }); - if (each.after) await each.after(); + await parseAssertion( + () => { + nodeAssert.ok(value); + }, + { message } + ); }; const equal = async ( @@ -19,9 +29,12 @@ const equal = async ( expected: unknown, message?: ParseAssertionOptions['message'] ): Promise => { - if (each.before) await each.before(); - parseAssertion(() => nodeAssert.equal(actual, expected), { message }); - if (each.after) await each.after(); + await parseAssertion( + () => { + nodeAssert.equal(actual, expected); + }, + { message } + ); }; const deepEqual = async ( @@ -29,11 +42,9 @@ const deepEqual = async ( expected: unknown, message?: ParseAssertionOptions['message'] ): Promise => { - if (each.before) await each.before(); - parseAssertion(() => nodeAssert.deepEqual(actual, expected), { + await parseAssertion(() => nodeAssert.deepEqual(actual, expected), { message, }); - if (each.after) await each.after(); }; const strictEqual = async ( @@ -41,11 +52,9 @@ const strictEqual = async ( expected: unknown, message?: ParseAssertionOptions['message'] ): Promise => { - if (each.before) await each.before(); - parseAssertion(() => nodeAssert.strictEqual(actual, expected), { + await parseAssertion(() => nodeAssert.strictEqual(actual, expected), { message, }); - if (each.after) await each.after(); }; const deepStrictEqual = async ( @@ -53,292 +62,153 @@ const deepStrictEqual = async ( expected: unknown, message?: ParseAssertionOptions['message'] ): Promise => { - if (each.before) await each.before(); - parseAssertion(() => nodeAssert.deepStrictEqual(actual, expected), { + await parseAssertion(() => nodeAssert.deepStrictEqual(actual, expected), { message, }); - if (each.after) await each.after(); }; -const doesNotMatch = async ( - value: string, - regExp: RegExp, +const notEqual = async ( + actual: unknown, + expected: unknown, message?: ParseAssertionOptions['message'] ): Promise => { - if (each.before) await each.before(); - parseAssertion(() => nodeAssert.doesNotMatch(value, regExp), { + await parseAssertion(() => nodeAssert.notEqual(actual, expected), { message, }); - if (each.after) await each.after(); }; -function doesNotReject( - block: (() => Promise) | Promise, - message?: string | Error -): Promise; -function doesNotReject( - block: (() => Promise) | Promise, - error: nodeAssert.AssertPredicate, - message?: string | Error -): Promise; -async function doesNotReject( - block: (() => Promise) | Promise, - errorOrMessage?: nodeAssert.AssertPredicate | string | Error, - message?: string | Error -): Promise { - if ( - typeof errorOrMessage === 'function' || - errorOrMessage instanceof RegExp || - typeof errorOrMessage === 'object' - ) { - try { - if (each.before) await each.before(); - await nodeAssert.doesNotReject(block, errorOrMessage); - if (each.after) await each.after(); - } catch (error) { - parseAssertion( - () => { - throw error; - }, - { - message, - defaultMessage: 'Got unwanted rejection', - hideDiff: true, - throw: true, - } - ); +const notDeepEqual = async ( + actual: unknown, + expected: unknown, + message?: ParseAssertionOptions['message'] +): Promise => { + await parseAssertion(() => nodeAssert.notDeepEqual(actual, expected), { + message, + }); +}; + +const notStrictEqual = async ( + actual: unknown, + expected: unknown, + message?: ParseAssertionOptions['message'] +): Promise => { + await parseAssertion(() => nodeAssert.notStrictEqual(actual, expected), { + message, + }); +}; + +const notDeepStrictEqual = async ( + actual: unknown, + expected: unknown, + message?: ParseAssertionOptions['message'] +): Promise => { + await parseAssertion(() => nodeAssert.notDeepStrictEqual(actual, expected), { + message, + }); +}; + +const ifError = async (value: unknown): Promise => { + await parseAssertion( + () => { + nodeAssert.ifError(value); + }, + { + defaultMessage: 'Expected no error, but received an error', + hideDiff: true, + throw: true, } - } else { - try { - if (each.before) await each.before(); - await nodeAssert.doesNotReject(block); - if (each.after) await each.after(); - } catch (error_1) { - parseAssertion( - () => { - throw error_1; - }, - { - message: - typeof errorOrMessage === 'string' ? errorOrMessage : undefined, - defaultMessage: 'Got unwanted rejection', - hideDiff: true, - throw: true, - } - ); + ); +}; + +const fail = async ( + message?: ParseAssertionOptions['message'] +): Promise => { + await parseAssertion( + () => { + nodeAssert.fail(message); + }, + { + message, + defaultMessage: 'Test failed intentionally', + hideDiff: true, } - } -} + ); +}; function doesNotThrow( block: () => unknown, message?: string | ParseAssertionOptions['message'] -): void; +): Promise; function doesNotThrow( block: () => unknown, error: nodeAssert.AssertPredicate, message?: ParseAssertionOptions['message'] -): void; +): Promise; async function doesNotThrow( block: () => unknown, errorOrMessage?: nodeAssert.AssertPredicate | string | Error, message?: ParseAssertionOptions['message'] -): Promise> { - if ( - typeof errorOrMessage === 'function' || - errorOrMessage instanceof RegExp || - typeof errorOrMessage === 'object' - ) { - try { - if (each.before) await each.before(); - nodeAssert.doesNotThrow(block, errorOrMessage, message); - if (each.after) await each.after(); - } catch (error) { - parseAssertion( - () => { - throw error; - }, - { - message: message, - defaultMessage: 'Expected function not to throw', - hideDiff: true, - throw: true, - } - ); - } - } else { - const msg = typeof errorOrMessage === 'string' ? errorOrMessage : undefined; - try { - if (each.before) await each.before(); - nodeAssert.doesNotThrow(block, msg); - if (each.after) await each.after(); - } catch (error) { - parseAssertion( - () => { - throw error; - }, - { - message: msg, - defaultMessage: 'Expected function not to throw', - hideDiff: true, - throw: true, - } - ); +): Promise { + await parseAssertion( + () => { + if ( + typeof errorOrMessage === 'function' || + errorOrMessage instanceof RegExp || + typeof errorOrMessage === 'object' + ) { + nodeAssert.doesNotThrow(block, errorOrMessage, message); + } else { + const msg = + typeof errorOrMessage === 'string' ? errorOrMessage : message; + nodeAssert.doesNotThrow(block, msg); + } + }, + { + message: typeof errorOrMessage === 'string' ? errorOrMessage : message, + defaultMessage: 'Expected function not to throw', + hideDiff: true, + throw: true, } - } + ); } function throws( block: () => unknown, message?: ParseAssertionOptions['message'] -): void; +): Promise; function throws( block: () => unknown, error: nodeAssert.AssertPredicate, message?: ParseAssertionOptions['message'] -): void; +): Promise; async function throws( block: () => unknown, errorOrMessage?: | nodeAssert.AssertPredicate | ParseAssertionOptions['message'], message?: ParseAssertionOptions['message'] -): Promise> { +): Promise { if ( typeof errorOrMessage === 'function' || errorOrMessage instanceof RegExp || typeof errorOrMessage === 'object' ) { - try { - if (each.before) await each.before(); - nodeAssert.throws(block, errorOrMessage, message); - if (each.after) await each.after(); - } catch (error) { - parseAssertion( - () => { - throw error; - }, - { - message: message, - defaultMessage: 'Expected function to throw', - hideDiff: true, - } - ); - } + await parseAssertion(() => nodeAssert.throws(block, errorOrMessage), { + message, + defaultMessage: 'Expected function to throw', + hideDiff: true, + }); } else { - const msg = typeof errorOrMessage === 'string' ? errorOrMessage : undefined; - try { - if (each.before) await each.before(); - nodeAssert.throws(block, message); - if (each.after) await each.after(); - } catch (error) { - parseAssertion( - () => { - throw error; - }, - { - message: msg, - defaultMessage: 'Expected function to throw', - hideDiff: true, - } - ); - } - } -} - -const notEqual = async ( - actual: unknown, - expected: unknown, - message?: ParseAssertionOptions['message'] -): Promise => { - if (each.before) await each.before(); - parseAssertion(() => nodeAssert.notEqual(actual, expected), { - message, - }); - if (each.after) await each.after(); -}; - -const notDeepEqual = async ( - actual: unknown, - expected: unknown, - message?: ParseAssertionOptions['message'] -): Promise => { - if (each.before) await each.before(); - parseAssertion(() => nodeAssert.notDeepEqual(actual, expected), { - message, - }); - if (each.after) await each.after(); -}; + const msg = + typeof errorOrMessage !== 'undefined' ? errorOrMessage : message; -const notStrictEqual = async ( - actual: unknown, - expected: unknown, - message?: ParseAssertionOptions['message'] -): Promise => { - if (each.before) await each.before(); - parseAssertion(() => nodeAssert.notStrictEqual(actual, expected), { - message, - }); - if (each.after) await each.after(); -}; - -const notDeepStrictEqual = async ( - actual: unknown, - expected: unknown, - message?: ParseAssertionOptions['message'] -): Promise => { - if (each.before) await each.before(); - parseAssertion(() => nodeAssert.notDeepStrictEqual(actual, expected), { - message, - }); - if (each.after) await each.after(); -}; - -const match = async ( - value: string, - regExp: RegExp, - message?: ParseAssertionOptions['message'] -): Promise => { - if (each.before) await each.before(); - parseAssertion(() => nodeAssert.match(value, regExp), { message }); - if (each.after) await each.after(); -}; - -const ifError = async (value: unknown): Promise => { - try { - if (each.before) await each.before(); - nodeAssert.ifError(value); - if (each.after) await each.after(); - } catch (error) { - parseAssertion( - () => { - throw error; - }, - { - defaultMessage: 'Expected no error, but received an error', - hideDiff: true, - throw: true, - } - ); + await parseAssertion(() => nodeAssert.throws(block, message), { + message: msg, + defaultMessage: 'Expected function to throw', + hideDiff: true, + }); } -}; - -const fail = (message?: ParseAssertionOptions['message']): void => { - try { - nodeAssert.fail(message); - } catch (error) { - parseAssertion( - () => { - throw error; - }, - { - message, - defaultMessage: 'Test failed intentionally', - hideDiff: true, - } - ); - } -}; +} function rejects( block: (() => Promise) | Promise, @@ -354,49 +224,96 @@ async function rejects( errorOrMessage?: nodeAssert.AssertPredicate | string | Error, message?: string | Error ): Promise { - if ( - typeof errorOrMessage === 'function' || - errorOrMessage instanceof RegExp || - typeof errorOrMessage === 'object' - ) { - try { - if (each.before) await each.before(); - await nodeAssert.rejects(block, errorOrMessage); - if (each.after) await each.after(); - } catch (error) { - parseAssertion( - () => { - throw error; - }, - { - message, - defaultMessage: - 'Expected promise to be rejected with specified error', - hideDiff: true, - } - ); + await parseAssertion( + async () => { + if ( + typeof errorOrMessage === 'function' || + errorOrMessage instanceof RegExp || + typeof errorOrMessage === 'object' + ) { + await nodeAssert.rejects(block, errorOrMessage, message); + } else { + const msg = + typeof errorOrMessage === 'string' ? errorOrMessage : message; + await nodeAssert.rejects(block, msg); + } + }, + { + message: typeof errorOrMessage === 'string' ? errorOrMessage : message, + defaultMessage: 'Expected promise to be rejected with specified error', + hideDiff: true, + throw: true, } - } else { - const msg = typeof errorOrMessage === 'string' ? errorOrMessage : undefined; - try { - if (each.before) await each.before(); - await nodeAssert.rejects(block); - if (each.after) await each.after(); - } catch (error_1) { - parseAssertion( - () => { - throw error_1; - }, - { - message: msg, - defaultMessage: 'Expected promise to be rejected', - hideDiff: true, - } - ); + ); +} + +function doesNotReject( + block: (() => Promise) | Promise, + message?: string | Error +): Promise; +function doesNotReject( + block: (() => Promise) | Promise, + error: nodeAssert.AssertPredicate, + message?: string | Error +): Promise; +async function doesNotReject( + block: (() => Promise) | Promise, + errorOrMessage?: nodeAssert.AssertPredicate | string | Error, + message?: string | Error +): Promise { + await parseAssertion( + async () => { + if ( + typeof errorOrMessage === 'function' || + errorOrMessage instanceof RegExp || + typeof errorOrMessage === 'object' + ) + await nodeAssert.doesNotReject(block, errorOrMessage, message); + else await nodeAssert.doesNotReject(block, message); + }, + { + message: typeof errorOrMessage === 'string' ? errorOrMessage : message, + defaultMessage: 'Got unwanted rejection', + hideDiff: true, + throw: true, } - } + ); } +const match = async ( + value: string, + regExp: RegExp, + message?: ParseAssertionOptions['message'] +): Promise => { + if (typeof version === 'number' && version < 12) { + throw new Error('doesNotMatch is available from Node.js 12 or higher'); + } + + await parseAssertion(() => nodeAssert?.match(value, regExp), { + message, + actual: 'Value', + expected: 'RegExp', + defaultMessage: 'Value should match regExp', + }); +}; + +const doesNotMatch = async ( + value: string, + regExp: RegExp, + message?: ParseAssertionOptions['message'] +): Promise => { + if (typeof version === 'number' && version < 12) { + throw new Error('doesNotMatch is available from Node.js 12 or higher'); + } + + await parseAssertion(() => nodeAssert.doesNotMatch(value, regExp), { + message, + actual: 'Value', + expected: 'RegExp', + defaultMessage: 'Value should not match regExp', + }); +}; + export const assertPromise = Object.assign( (value: unknown, message?: ParseAssertionOptions['message']) => ok(value, message), diff --git a/src/modules/assert.ts b/src/modules/assert.ts index 8a038399..670c1bf8 100644 --- a/src/modules/assert.ts +++ b/src/modules/assert.ts @@ -1,26 +1,27 @@ +import process from 'node:process'; import * as nodeAssert from 'node:assert'; import { parseAssertion, ParseAssertionOptions, } from '../helpers/parseAsssetion.js'; +import { getRuntime } from '../helpers/get-runtime.js'; -type Each = { - before?: undefined | (() => unknown); - after?: undefined | (() => unknown); -}; - -export const each: Each = { - before: undefined, - after: undefined, -}; +const runtime = getRuntime(); +const version = + runtime === 'node' + ? Number(process.version.match(/v(\d+)\./)?.[1]) + : undefined; const ok = ( value: unknown, message?: ParseAssertionOptions['message'] ): void => { - each.before && each.before(); - parseAssertion(() => nodeAssert.ok(value), { message }); - each.after && each.after(); + parseAssertion( + () => { + nodeAssert.ok(value); + }, + { message } + ); }; const equal = ( @@ -28,9 +29,12 @@ const equal = ( expected: unknown, message?: ParseAssertionOptions['message'] ): void => { - each.before && each.before(); - parseAssertion(() => nodeAssert.equal(actual, expected), { message }); - each.after && each.after(); + parseAssertion( + () => { + nodeAssert.equal(actual, expected); + }, + { message } + ); }; const deepEqual = ( @@ -38,9 +42,7 @@ const deepEqual = ( expected: unknown, message?: ParseAssertionOptions['message'] ): void => { - each.before && each.before(); parseAssertion(() => nodeAssert.deepEqual(actual, expected), { message }); - each.after && each.after(); }; const strictEqual = ( @@ -48,9 +50,7 @@ const strictEqual = ( expected: unknown, message?: ParseAssertionOptions['message'] ): void => { - each.before && each.before(); parseAssertion(() => nodeAssert.strictEqual(actual, expected), { message }); - each.after && each.after(); }; const deepStrictEqual = ( @@ -58,85 +58,74 @@ const deepStrictEqual = ( expected: unknown, message?: ParseAssertionOptions['message'] ): void => { - each.before && each.before(); parseAssertion(() => nodeAssert.deepStrictEqual(actual, expected), { message, }); - each.after && each.after(); }; -const doesNotMatch = ( - value: string, - regExp: RegExp, +const notEqual = ( + actual: unknown, + expected: unknown, message?: ParseAssertionOptions['message'] ): void => { - each.before && each.before(); - parseAssertion(() => nodeAssert.doesNotMatch(value, regExp), { + parseAssertion(() => nodeAssert.notEqual(actual, expected), { message, - actual: 'Value', - expected: 'RegExp', - defaultMessage: 'Value should not match regExp', }); - each.after && each.after(); }; -function doesNotReject( - block: (() => Promise) | Promise, - message?: string | Error -): Promise; -function doesNotReject( - block: (() => Promise) | Promise, - error: nodeAssert.AssertPredicate, - message?: string | Error -): Promise; -async function doesNotReject( - block: (() => Promise) | Promise, - errorOrMessage?: nodeAssert.AssertPredicate | string | Error, - message?: string | Error -): Promise { - if ( - typeof errorOrMessage === 'function' || - errorOrMessage instanceof RegExp || - typeof errorOrMessage === 'object' - ) { - try { - each.before && each.before(); - await nodeAssert.doesNotReject(block, errorOrMessage); - each.after && each.after(); - } catch (error) { - parseAssertion( - () => { - throw error; - }, - { - message, - defaultMessage: 'Got unwanted rejection', - hideDiff: true, - throw: true, - } - ); +const notDeepEqual = ( + actual: unknown, + expected: unknown, + message?: ParseAssertionOptions['message'] +): void => { + parseAssertion(() => nodeAssert.notDeepEqual(actual, expected), { message }); +}; + +const notStrictEqual = ( + actual: unknown, + expected: unknown, + message?: ParseAssertionOptions['message'] +): void => { + parseAssertion(() => nodeAssert.notStrictEqual(actual, expected), { + message, + }); +}; + +const notDeepStrictEqual = ( + actual: unknown, + expected: unknown, + message?: ParseAssertionOptions['message'] +): void => { + parseAssertion(() => nodeAssert.notDeepStrictEqual(actual, expected), { + message, + }); +}; + +const ifError = (value: unknown): void => { + parseAssertion( + () => { + nodeAssert.ifError(value); + }, + { + defaultMessage: 'Expected no error, but received an error', + hideDiff: true, + throw: true, } - } else { - try { - each.before && each.before(); - await nodeAssert.doesNotReject(block); - each.after && each.after(); - } catch (error_1) { - parseAssertion( - () => { - throw error_1; - }, - { - message: - typeof errorOrMessage === 'string' ? errorOrMessage : undefined, - defaultMessage: 'Got unwanted rejection', - hideDiff: true, - throw: true, - } - ); + ); +}; + +const fail = (message?: ParseAssertionOptions['message']): void => { + parseAssertion( + () => { + nodeAssert.fail(message); + }, + { + message, + defaultMessage: 'Test failed intentionally', + hideDiff: true, } - } -} + ); +}; function doesNotThrow( block: () => unknown, @@ -152,48 +141,27 @@ function doesNotThrow( errorOrMessage?: nodeAssert.AssertPredicate | string | Error, message?: ParseAssertionOptions['message'] ): void { - if ( - typeof errorOrMessage === 'function' || - errorOrMessage instanceof RegExp || - typeof errorOrMessage === 'object' - ) { - try { - each.before && each.before(); - nodeAssert.doesNotThrow(block, errorOrMessage, message); - each.after && each.after(); - } catch (error) { - parseAssertion( - () => { - throw error; - }, - { - message: message, - defaultMessage: 'Expected function not to throw', - hideDiff: true, - throw: true, - } - ); - } - } else { - const msg = typeof errorOrMessage === 'string' ? errorOrMessage : undefined; - try { - each.before && each.before(); - nodeAssert.doesNotThrow(block, msg); - each.after && each.after(); - } catch (error) { - parseAssertion( - () => { - throw error; - }, - { - message: msg, - defaultMessage: 'Expected function not to throw', - hideDiff: true, - throw: true, - } - ); + parseAssertion( + () => { + if ( + typeof errorOrMessage === 'function' || + errorOrMessage instanceof RegExp || + typeof errorOrMessage === 'object' + ) { + nodeAssert.doesNotThrow(block, errorOrMessage, message); + } else { + const msg = + typeof errorOrMessage === 'string' ? errorOrMessage : message; + nodeAssert.doesNotThrow(block, msg); + } + }, + { + message: typeof errorOrMessage === 'string' ? errorOrMessage : message, + defaultMessage: 'Expected function not to throw', + hideDiff: true, + throw: true, } - } + ); } function throws( @@ -217,197 +185,127 @@ function throws( errorOrMessage instanceof RegExp || typeof errorOrMessage === 'object' ) { - try { - each.before && each.before(); - nodeAssert.throws(block, errorOrMessage, message); - each.after && each.after(); - } catch (error) { - parseAssertion( - () => { - throw error; - }, - { - message: message, - defaultMessage: 'Expected function to throw', - hideDiff: true, - } - ); - } + parseAssertion(() => nodeAssert.throws(block, errorOrMessage), { + message, + defaultMessage: 'Expected function to throw', + hideDiff: true, + }); } else { - const msg = typeof errorOrMessage === 'string' ? errorOrMessage : undefined; - try { - each.before && each.before(); - nodeAssert.throws(block, message); - each.after && each.after(); - } catch (error) { - parseAssertion( - () => { - throw error; - }, - { - message: msg, - defaultMessage: 'Expected function to throw', - hideDiff: true, - } - ); - } + const msg = + typeof errorOrMessage !== 'undefined' ? errorOrMessage : message; + + parseAssertion(() => nodeAssert.throws(block, message), { + message: msg, + defaultMessage: 'Expected function to throw', + hideDiff: true, + }); } } -const notEqual = ( - actual: unknown, - expected: unknown, - message?: ParseAssertionOptions['message'] -): void => { - each.before && each.before(); - parseAssertion(() => nodeAssert.notEqual(actual, expected), { - message, - }); - each.after && each.after(); -}; +function rejects( + block: (() => Promise) | Promise, + message?: string | Error +): Promise; +function rejects( + block: (() => Promise) | Promise, + error: nodeAssert.AssertPredicate, + message?: string | Error +): Promise; +async function rejects( + block: (() => Promise) | Promise, + errorOrMessage?: nodeAssert.AssertPredicate | string | Error, + message?: string | Error +): Promise { + await parseAssertion( + async () => { + if ( + typeof errorOrMessage === 'function' || + errorOrMessage instanceof RegExp || + typeof errorOrMessage === 'object' + ) { + await nodeAssert.rejects(block, errorOrMessage, message); + } else { + const msg = + typeof errorOrMessage === 'string' ? errorOrMessage : message; + await nodeAssert.rejects(block, msg); + } + }, + { + message: typeof errorOrMessage === 'string' ? errorOrMessage : message, + defaultMessage: 'Expected promise to be rejected with specified error', + hideDiff: true, + throw: true, + } + ); +} -const notDeepEqual = ( - actual: unknown, - expected: unknown, - message?: ParseAssertionOptions['message'] -): void => { - each.before && each.before(); - parseAssertion(() => nodeAssert.notDeepEqual(actual, expected), { message }); - each.after && each.after(); -}; +function doesNotReject( + block: (() => Promise) | Promise, + message?: string | Error +): Promise; +function doesNotReject( + block: (() => Promise) | Promise, + error: nodeAssert.AssertPredicate, + message?: string | Error +): Promise; +async function doesNotReject( + block: (() => Promise) | Promise, + errorOrMessage?: nodeAssert.AssertPredicate | string | Error, + message?: string | Error +): Promise { + await parseAssertion( + async () => { + if ( + typeof errorOrMessage === 'function' || + errorOrMessage instanceof RegExp || + typeof errorOrMessage === 'object' + ) + await nodeAssert.doesNotReject(block, errorOrMessage, message); + else await nodeAssert.doesNotReject(block, message); + }, + { + message: typeof errorOrMessage === 'string' ? errorOrMessage : message, + defaultMessage: 'Got unwanted rejection', + hideDiff: true, + throw: true, + } + ); +} -const notStrictEqual = ( - actual: unknown, - expected: unknown, +const match = ( + value: string, + regExp: RegExp, message?: ParseAssertionOptions['message'] ): void => { - each.before && each.before(); - parseAssertion(() => nodeAssert.notStrictEqual(actual, expected), { - message, - }); - each.after && each.after(); -}; + if (typeof version === 'number' && version < 12) { + throw new Error('doesNotMatch is available from Node.js 12 or higher'); + } -const notDeepStrictEqual = ( - actual: unknown, - expected: unknown, - message?: ParseAssertionOptions['message'] -): void => { - each.before && each.before(); - parseAssertion(() => nodeAssert.notDeepStrictEqual(actual, expected), { + parseAssertion(() => nodeAssert?.match(value, regExp), { message, + actual: 'Value', + expected: 'RegExp', + defaultMessage: 'Value should match regExp', }); - each.after && each.after(); }; -const match = ( +const doesNotMatch = ( value: string, regExp: RegExp, message?: ParseAssertionOptions['message'] ): void => { - each.before && each.before(); - parseAssertion(() => nodeAssert.match(value, regExp), { + if (typeof version === 'number' && version < 12) { + throw new Error('doesNotMatch is available from Node.js 12 or higher'); + } + + parseAssertion(() => nodeAssert.doesNotMatch(value, regExp), { message, actual: 'Value', expected: 'RegExp', - defaultMessage: 'Value should match regExp', + defaultMessage: 'Value should not match regExp', }); - each.after && each.after(); -}; - -const ifError = (value: unknown): void => { - try { - each.before && each.before(); - nodeAssert.ifError(value); - each.after && each.after(); - } catch (error) { - parseAssertion( - () => { - throw error; - }, - { - defaultMessage: 'Expected no error, but received an error', - hideDiff: true, - throw: true, - } - ); - } }; -const fail = (message?: ParseAssertionOptions['message']): void => { - try { - nodeAssert.fail(message); - } catch (error) { - parseAssertion( - () => { - throw error; - }, - { - message, - defaultMessage: 'Test failed intentionally', - hideDiff: true, - } - ); - } -}; - -function rejects( - block: (() => Promise) | Promise, - message?: string | Error -): Promise; -function rejects( - block: (() => Promise) | Promise, - error: nodeAssert.AssertPredicate, - message?: string | Error -): Promise; -async function rejects( - block: (() => Promise) | Promise, - errorOrMessage?: nodeAssert.AssertPredicate | string | Error, - message?: string | Error -): Promise { - if ( - typeof errorOrMessage === 'function' || - errorOrMessage instanceof RegExp || - typeof errorOrMessage === 'object' - ) { - try { - each.before && each.before(); - await nodeAssert.rejects(block, errorOrMessage); - each.after && each.after(); - } catch (error) { - parseAssertion( - () => { - throw error; - }, - { - message, - defaultMessage: - 'Expected promise to be rejected with specified error', - hideDiff: true, - } - ); - } - } else { - const msg = typeof errorOrMessage === 'string' ? errorOrMessage : undefined; - try { - each.before && each.before(); - await nodeAssert.rejects(block); - each.after && each.after(); - } catch (error_1) { - parseAssertion( - () => { - throw error_1; - }, - { - message: msg, - defaultMessage: 'Expected promise to be rejected', - hideDiff: true, - } - ); - } - } -} - export const assert = Object.assign( (value: unknown, message?: ParseAssertionOptions['message']) => ok(value, message), diff --git a/src/modules/describe.ts b/src/modules/describe.ts new file mode 100644 index 00000000..5787d7d9 --- /dev/null +++ b/src/modules/describe.ts @@ -0,0 +1,46 @@ +import { EOL } from 'node:os'; +import { format, backgroundColor } from '../helpers/format.js'; + +export type DescribeOptions = { + /** + * Skips a line before to console it. + * + * @default false + */ + pad?: boolean; + /** + * @default "grey" + */ + background?: keyof typeof backgroundColor | boolean; + /** + * @default "☰" + */ + icon?: string; +}; + +/** + * By default **Poku** only shows outputs generated from itself. + * This helper allows you to use an alternative to `console.log` with **Poku**. + * + * Need to debug? Just use the [`debug`](https://poku.io/docs/documentation/poku/configs/debug) option from `poku`. + */ +export const log = (message: string) => console.log(`\x1b[0m${message}\x1b[0m`); + +/** + * On **Poku**, `describe` is just a pretty `console.log` to title your test suites in the terminal. + */ +export const describe = (title: string, options?: DescribeOptions) => { + const { pad, background, icon } = options || {}; + + const message = `${icon || '☰'} ${title}`; + const noBackground = typeof background === 'boolean' && !background; + + if (noBackground) { + console.log(`${pad ? EOL : ''}${format.bold(message)}`); + return; + } + + console.log( + `${pad ? EOL : ''}${format.bg(backgroundColor[typeof background === 'string' ? background : 'grey'], message)}` + ); +}; diff --git a/src/modules/each.ts b/src/modules/each.ts index 85f17677..d13a6866 100644 --- a/src/modules/each.ts +++ b/src/modules/each.ts @@ -1,20 +1,13 @@ -import { each } from '../modules/assert.js'; +import { Control, each } from '../configs/each.js'; -type Control = { - pause: () => void; - continue: () => void; - reset: () => void; -}; - -const status = { - before: true, - after: true, +type EachOptions = { + immediate?: boolean; }; /** * - ✅ Handling **global** and **external** services (_preparing a database, for example_) * - ✅ It's made for **exclusive use** in combination with **Poku**'s **`assert`** methods - * - ❌ Changing local variables values and states ([_use a mock instead_](https://poku.io/docs/category/mock)) + * - ⚠️ Although `beforeEach` accepts local variables changes by using the `imediate` option, it's strongly discouraged ([_use a mock instead_](https://poku.io/docs/category/mock)) * * --- * @@ -32,21 +25,26 @@ const status = { * before.reset(); * ``` */ -export const beforeEach = (callback: () => unknown): Control => { - each.before = () => { - if (status.before) callback(); +export const beforeEach = ( + callback: () => unknown, + options?: EachOptions +): Control => { + options?.immediate && callback(); + + each.before.cb = () => { + if (each.before.status) callback(); }; const pause = () => { - status.before = false; + each.before.status = false; }; const continueFunc = () => { - status.before = true; + each.before.status = true; }; const reset = () => { - each.before = undefined; + each.before.cb = undefined; }; return { pause, continue: continueFunc, reset }; @@ -55,7 +53,7 @@ export const beforeEach = (callback: () => unknown): Control => { /** * - ✅ Handling **global** and **external** services (_preparing a database, for example_) * - ✅ It's made for **exclusive use** in combination with **Poku**'s **`assert`** methods - * - ❌ Changing local variables values and states ([_use a mock instead_](https://poku.io/docs/category/mock)) + * - ⚠️ Although `afterEach` accepts local variables changes, it's strongly discouraged ([_use a mock instead_](https://poku.io/docs/category/mock)) * * --- * @@ -74,20 +72,20 @@ export const beforeEach = (callback: () => unknown): Control => { * ``` */ export const afterEach = (callback: () => unknown): Control => { - each.after = () => { - if (status.after) callback(); + each.after.cb = () => { + if (each.after.status) callback(); }; const pause = () => { - status.after = false; + each.after.status = false; }; const continueFunc = () => { - status.after = true; + each.after.status = true; }; const reset = () => { - each.after = undefined; + each.after.cb = undefined; }; return { pause, continue: continueFunc, reset }; diff --git a/test/docker/playground/deno/Dockerfile b/test/docker/playground/deno/Dockerfile new file mode 100644 index 00000000..554eb0c8 --- /dev/null +++ b/test/docker/playground/deno/Dockerfile @@ -0,0 +1,11 @@ +FROM denoland/deno:alpine-1.30.0 + +WORKDIR /usr/app + +COPY ./src ./src +COPY ./test ./test +COPY ./tools ./tools + +RUN deno run --allow-read --allow-write --allow-env --allow-run tools/compatibility/deno.ts + +CMD ["tail", "-f", "/dev/null"] diff --git a/test/docker/playground/deno/docker-compose.yml b/test/docker/playground/deno/docker-compose.yml new file mode 100644 index 00000000..e0f75b5e --- /dev/null +++ b/test/docker/playground/deno/docker-compose.yml @@ -0,0 +1,10 @@ +version: '3.9' +services: + playground-deno: + container_name: playground-deno + build: + context: ${PWD} + dockerfile: ./test/docker/playground/deno/Dockerfile + working_dir: /usr/app + stdin_open: true + tty: true diff --git a/website/docs/documentation/helpers/before-after-each/_category_.json b/website/docs/documentation/helpers/before-after-each/_category_.json index f1e54e5f..93043d1d 100644 --- a/website/docs/documentation/helpers/before-after-each/_category_.json +++ b/website/docs/documentation/helpers/before-after-each/_category_.json @@ -4,5 +4,5 @@ "link": { "type": "generated-index" }, - "position": 3 + "position": 2 } diff --git a/website/docs/documentation/helpers/before-after-each/in-code.mdx b/website/docs/documentation/helpers/before-after-each/in-code.mdx index 703d2062..cce908f0 100644 --- a/website/docs/documentation/helpers/before-after-each/in-code.mdx +++ b/website/docs/documentation/helpers/before-after-each/in-code.mdx @@ -36,13 +36,30 @@ For example, by populating or resetting a database before and/or after multiple - ✅ Handling **global** and **external** services (_preparing a database, for example_) - ✅ It's made for **exclusive use** in combination with **Poku**'s **`assert`** methods -- ✅ You can combine `beforeEach`, `afetrEach` and all `assert` methods, except for `assert.fail(message?: string)`. +- ✅ You can combine `beforeEach`, `afterEach` and all `assert` methods, except for `assert.fail(message?: string)`. ::: -:::danger +:::info -- ❌ Don't change local variables and states inside `beforeEach` or `afterEach` ([_use a mock instead_](/docs/category/mock)) +- Although `beforeEach` and `afterEach` accepts local variables changes, it's strongly encouraged ([_you can use a mock instead_](/docs/category/mock)). + +See why (_note the `immediate` option_): + +```ts +import { assert, beforeEach, afterEach } from 'poku'; + +let value = 0; + +beforeEach(() => ++value, { immediate: true }); +afterEach(() => ++value); + +assert.equal(value, 1); // ✅ +assert.equal(value, 3); // ✅ + +// ✋ In the `eachBefore` context, `value` is now `4`, while locally it's `5` +console.log(value); +``` ::: @@ -139,13 +156,32 @@ You can overwriting both `beforeEach` and `afterEach` by declaring them again an - ✅ Handling **global** and **external** services (_preparing a database, for example_) - ✅ It's made for **exclusive use** in combination with **Poku**'s **`assert`** methods -- ✅ You can combine `beforeEach`, `afetrEach` and all `assert` methods, except for `assert.fail(message?: string)`. +- ✅ You can combine `beforeEach`, `afterEach` and all `assert` methods, except for `assert.fail(message?: string)`. ::: -:::danger +:::info + +- Although `beforeEach` and `afterEach` accepts local variables changes, it's strongly encouraged ([_you can use a mock instead_](/docs/category/mock)). + +See why (_note the `immediate` option_): + +```ts +import { assertPromise as assert, beforeEach, afterEach } from 'poku'; + +let value = 0; + +beforeEach(async () => new Promise((resolve) => resolve(++value)), { + immediate: true, +}); +afterEach(async () => new Promise((resolve) => resolve(++value))); + +await assert.equal(value, 1); // ✅ +await assert.equal(value, 3); // ✅ -- ❌ Don't change local variables and states inside `beforeEach` or `afterEach` ([_use a mock instead_](/docs/category/mock)) +// ✋ In the `eachBefore` context, `value` is now `4`, while locally it's `5` +console.log(value); +``` ::: diff --git a/website/docs/documentation/helpers/before-after-each/per-file.mdx b/website/docs/documentation/helpers/before-after-each/per-file.mdx index acdbedcb..6a1083f0 100644 --- a/website/docs/documentation/helpers/before-after-each/per-file.mdx +++ b/website/docs/documentation/helpers/before-after-each/per-file.mdx @@ -54,5 +54,5 @@ import Failure from '@site/static/img/each-fail.png';
:::info -Although it also works with `parallel` runs, it's strongly encouraged to use these features for sequential tests. +Although it also works with `parallel` runs, it's strongly discouraged to use these features for sequential tests. ::: diff --git a/website/docs/documentation/helpers/describe.mdx b/website/docs/documentation/helpers/describe.mdx new file mode 100644 index 00000000..8fc16509 --- /dev/null +++ b/website/docs/documentation/helpers/describe.mdx @@ -0,0 +1,124 @@ +--- +sidebar_position: 1 +--- + +import { FAQ } from '@site/src/components/FAQ'; +import Example from '@site/static/img/describe-example.png'; + +# describe + +On **Poku**, `describe` is just a pretty `console.log` to title your test suites in the terminal. + +```ts +import { describe, assert } from 'poku'; + +describe('Group A'); +assert(true, '1'); +assert(true, '2'); + +describe('Group B'); +assert(true, '1'); +assert(true, '2'); +``` + +## Personalization + +> `describe(title: string, options?: DescribeOptions)` + +### pad + +Skips a line before to console it. + +```ts +import { describe, assert } from 'poku'; + +describe('Group A', { pad: true }); +assert.ok(true, '1'); +assert.ok(true, '2'); +``` + +### background + +Change the background color for your personal title. + +> Set `false` to disable it. + +```ts +import { describe, assert } from 'poku'; + +describe('Group A', { background: 'blue' }); +assert.ok(true, '1'); +assert.ok(true, '2'); +``` + + + +- `white` +- `black` +- `grey` +- `red` +- `green` +- `yellow` +- `blue` +- `magenta` +- `cyan` +- `brightRed` +- `brightGreen` +- `brightYellow` +- `brightBlue` +- `brightMagenta` +- `brightCyan` + + + +### icon (prefix) + +**Poku** also allows the prefix customization. + +> The default icon is `☰`. + +```ts +import { describe, assert } from 'poku'; + +describe('Group A', { icon: '🚀' }); +assert.ok(true, '1'); +assert.ok(true, '2'); +``` + +
+ +## Overview + + + + ```ts + import { assert, describe } from 'poku'; + + describe('Needs to Succeed', { + pad: true, + background: false, + icon: '🚀', + }); + + assert.ok(true, 'Test 1'); + assert.ok(true, 'Test 2'); + + describe('Needs to Fail', { + pad: true, + background: 'yellow', + icon: '🚫', + }); + + assert.throws(() => { throw new Error() }, 'Test 1'); + assert.throws(() => { throw new Error() }, 'Test 2'); + ``` + + + + + + + +
+ +
diff --git a/website/docs/documentation/helpers/log.mdx b/website/docs/documentation/helpers/log.mdx new file mode 100644 index 00000000..009c1639 --- /dev/null +++ b/website/docs/documentation/helpers/log.mdx @@ -0,0 +1,19 @@ +--- +sidebar_position: 3 +--- + +# log + +Since by default **Poku** only shows outputs generated from itself, this helper allows you to use an alternative to `console.log` with **Poku** runner. + +```ts +import { log } from 'poku'; + +log('Poku will show it'); + +console.log("Poku won't show that"); +``` + +:::tip +Need to debug? Just use the [`debug`](/docs/documentation/poku/configs/debug) option from `poku`. +::: diff --git a/website/docs/index.mdx b/website/docs/index.mdx index 5f220288..fb5f2573 100644 --- a/website/docs/index.mdx +++ b/website/docs/index.mdx @@ -178,7 +178,7 @@ deno run npm:poku -### That's it 🎉 +### **That's it** 🎉 - [**See the complete `assert`'s documentation**](/docs/documentation/assert). - [**See the complete `poku`'s documentation**](/docs/category/poku). diff --git a/website/docs/philosophy.mdx b/website/docs/philosophy.mdx index 7196a12c..f705e651 100644 --- a/website/docs/philosophy.mdx +++ b/website/docs/philosophy.mdx @@ -6,6 +6,9 @@ Moreover, **Poku** does away with the headache of dealing with different environments like **CJS** or **ESM**, as it adapts to any setting (even [**TypeScript**][typescript-url] without compilation when paired with [**tsx**][tsx]). +Also, **Poku** doesn't use a global state, allowing you to use it how and where you want. + +[![Install Size](https://packagephobia.com/badge?p=poku)](https://packagephobia.com/result?p=poku)
Is it a cloud? No, it's **Poku**! It's not just lightweight; it doesn't even have external dependencies, allowing you to only add advanced features when really necessary. > Adopt a **Poku** for yourself 🐷 diff --git a/website/static/img/describe-example.png b/website/static/img/describe-example.png new file mode 100644 index 00000000..0151c7b9 Binary files /dev/null and b/website/static/img/describe-example.png differ