diff --git a/.github/assets/readme/assert-node.png b/.github/assets/readme/assert-node.png new file mode 100644 index 00000000..325df943 Binary files /dev/null and b/.github/assets/readme/assert-node.png differ diff --git a/.github/assets/readme/assert-poku.png b/.github/assets/readme/assert-poku.png new file mode 100644 index 00000000..b28d0279 Binary files /dev/null and b/.github/assets/readme/assert-poku.png differ diff --git a/.github/assets/readme/assert.png b/.github/assets/readme/assert.png deleted file mode 100644 index 5a555227..00000000 Binary files a/.github/assets/readme/assert.png and /dev/null differ diff --git a/.github/assets/readme/parallel.png b/.github/assets/readme/parallel.png index 8bf45626..68016334 100644 Binary files a/.github/assets/readme/parallel.png and b/.github/assets/readme/parallel.png differ diff --git a/.github/assets/readme/sequential.png b/.github/assets/readme/sequential.png index 74aa23cf..41a1d901 100644 Binary files a/.github/assets/readme/sequential.png and b/.github/assets/readme/sequential.png differ diff --git a/.gitignore b/.gitignore index db583d27..44c8da11 100755 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules .DS_Store /lib /ci +/playground .env diff --git a/README.md b/README.md index 29a43486..93f69d16 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,15 @@ Enjoying **Poku**? Consider giving him a star ⭐️ --- -🐷 [**Documentation Website**](https://poku.dev) • 🔬 [**Comparing Test Runners** (**Poku**, **Jest**, **Mocha**, **Vitest** and **AVA**)](https://poku.dev/docs/comparing) +🐷 [**Documentation Website**](https://poku.dev) • 🔬 [**Compare Poku with the Most Popular Test Runners**](https://poku.dev/docs/comparing) --- ## Why Poku? -> **Poku** starts from the premise where tests come to help, not overcomplicate: runs test files in an individual process per file, shows progress and exits 🧙🏻 +Don't worry about `describe`, `it`, `beforeEach` and everything else 🚀 + +> You don't need to learn what you already know ✨ - Supports **ESM** and **CJS** - Designed to be highly intuitive @@ -43,45 +45,61 @@ Enjoying **Poku**? Consider giving him a star ⭐️ - Allows both **in-code** and **CLI** usage - [**Node.js**][node-version-url], [**Bun**][bun-version-url] and [**Deno**][deno-version-url] compatibility - Zero configurations, except you want -- No constraints or rules, code in your own signature style +- Poku adapts to your test, not the other way around - [**And much more!**](https://poku.dev) --- -- -- **Zero** external dependencies +- +- **Zero** external dependencies 🌱 --- ## Documentation -- See detailed specifications and usage in [**Documentation**](https://poku.dev/docs/category/documentation) section for queries, advanced concepts and much more. +- See detailed usage in [**Documentation**](https://poku.dev/docs/category/documentation) section for **Poku**'s **CLI**, **API (_in-code_)** and **assert**, advanced concepts and much more. --- ## Overview -| Sequential | Parallel | -| ------------------------------------------------------------ | ---------------------------------------------------------- | -| `npx poku test/unit,test/integration` | `npx poku --parallel test/unit,test/integration` | -| | | +| Sequential | Concurrent | +| -------------------------------------------------- | ------------------------------------------------ | +| | | + +- By default, **Poku**: + - Searches for all _`.test.`_ and `.spec.` files, but you can customize it using the option [**`filter`**](https://poku.dev/docs/documentation/poku/configs/filter). + - Uses `sequential` mode. +- You can use concurrecy by use the flag `--parallel` for **CLI** or the option `parallel` to `true` in **API** (_in-code_) usage. -- By default, **Poku** searches for all _`.test.`_ files, but you can customize it using the option [`filter`](https://github.com/wellwelwel/poku#filter-rexexp). -- The same idea for [**Bun**][bun-version-url] and [**Deno**][deno-version-url] (see bellow). +> Follow the same idea for [**Bun**][bun-version-url] and [**Deno**][deno-version-url]. --- -**Poku** also includes the `assert` method, keeping everything as it is, but providing human readability: +**Poku** also includes the `assert` method, keeping everything as it is, but providing human readability and automatic `describe` and `it`: + +> Compatible with **Node.js**, **Bun** and **Deno**. ```ts import { assert } from 'poku'; // Node and Bun import { assert } from 'npm:poku'; // Deno -assert(true); -assert.deepStrictEqual(1, '1', 'My optional custom message'); +const actual = '1'; + +assert(actual, 'My first assert'); +assert.deepStrictEqual(actual, 1, 'My first assert error'); ``` -> +| Using `poku` | Using `node` | +| --------------------------------------------------- | --------------------------------------------------- | +| | | + +- ❌ Both cases finish with `code 1`, as expected +- 🧑🏻‍🎓 The `message` param is optional, as it's in **Node.js** +- 💚 Yes, you can use **Poku**'s `assert` running `node ./my-file.js` +- 🐷 Unlike most, **Poku** adapts to your test, not the other way around + +> [**See the complete assert's documentation**](https://poku.dev/docs/documentation/assert). --- @@ -89,66 +107,48 @@ assert.deepStrictEqual(1, '1', 'My optional custom message'); ### **Node.js** -> - ```bash -npm install --save-dev poku +npm i -D poku ``` ### TypeScript (Node.js) -> -> -> - ```bash -npm install --save-dev poku tsx +npm i -D poku tsx ``` ### Bun -> -> -> - ```bash -bun add --dev poku +bun add -d poku ``` ### **Deno** -> -> -> - ```ts import { poku } from 'npm:poku'; ``` -- **Poku** requires these permissions by default: `--allow-read`, `--allow-env` and `--allow-run`. - --- ## Quick Start ### In-code -> -> -> +#### Node.js and Bun ```ts import { poku } from 'poku'; -await poku(['targetDirA', 'targetDirB']); +await poku(['targetDir']); ``` -> +#### Deno ```ts import { poku } from 'npm:poku'; -await poku(['targetDirA', 'targetDirB']); +await poku(['targetDir']); ``` ### CLI @@ -156,19 +156,19 @@ await poku(['targetDirA', 'targetDirB']); > ```bash -npx poku targetDirA,targetDirB +npx poku targetDir ``` > ```bash -bun poku targetDirA,targetDirB +bun poku targetDir ``` > ```bash -deno run npm:poku targetDirA,targetDirB +deno run npm:poku targetDir ``` --- diff --git a/goals.md b/goals.md index acf5056e..39617c89 100644 --- a/goals.md +++ b/goals.md @@ -1,11 +1,12 @@ -# Goals +# 🧑🏻‍🎓 Goals -## `fix:` +## 🧑🏻‍🔧 `fix` + +- **assert:** improve logs for regex and functions inside arrays --- -## `feat:` +## 🚀 `feat` -- **feat:** show message from `assert` like in popular "describe" and "it" if `describe` option is `true` -- **feat:** show individual test execution time -- **feat:** allow to limit concurrency in parallel runs +- **cli:** show individual test execution time +- **poku:** allow to limit concurrency in parallel runs diff --git a/src/@types/poku.ts b/src/@types/poku.ts index 9f2fe645..9f51dc09 100644 --- a/src/@types/poku.ts +++ b/src/@types/poku.ts @@ -10,14 +10,19 @@ export type Configs = { */ noExit?: boolean; /** + * @deprecated * Customize `stdout` options. */ log?: { /** + * @deprecated + * * @default false */ success?: boolean; /** + * @deprecated + * * @default true */ fail?: boolean; @@ -27,6 +32,12 @@ export type Configs = { * * @default false */ + debug?: boolean; + /** + * This option overwrites the `debug` settings. + * + * @default false + */ quiet?: boolean; /** * Determines the mode of test execution. diff --git a/src/bin/index.ts b/src/bin/index.ts index 83c04856..b856da3a 100644 --- a/src/bin/index.ts +++ b/src/bin/index.ts @@ -4,6 +4,7 @@ import { escapeRegExp } from '../modules/list-files.js'; import { getArg, getLastParam, hasArg } from '../helpers/get-arg.js'; import { poku } from '../index.js'; import { platformIsValid } from '../helpers/get-runtime.js'; +import { format } from '../helpers/format.js'; const dirs = (hasArg('include') @@ -14,7 +15,12 @@ const filter = getArg('filter'); const exclude = getArg('exclude'); const parallel = hasArg('parallel'); const quiet = hasArg('quiet'); -const logSuccess = hasArg('log-success'); +const debug = hasArg('debug'); + +if (hasArg('log-success')) + console.log( + `The flag ${format.bold('--log-success')} is deprecated. Use ${format.bold('--debug')} instead.` + ); poku(dirs, { platform: platformIsValid(platform) ? platform : undefined, @@ -22,7 +28,5 @@ poku(dirs, { exclude: exclude ? new RegExp(escapeRegExp(exclude)) : undefined, parallel, quiet, - log: { - success: logSuccess, - }, + debug, }); diff --git a/src/helpers/format.ts b/src/helpers/format.ts index 3d27600e..936f658a 100644 --- a/src/helpers/format.ts +++ b/src/helpers/format.ts @@ -8,9 +8,15 @@ export const format = { dim: (value: string) => `\x1b[2m${value}\x1b[0m`, bold: (value: string) => `\x1b[1m${value}\x1b[0m`, underline: (value: string) => `\x1b[4m${value}\x1b[0m`, - info: (value: string) => `\x1b[36m${value}\x1b[0m`, + info: (value: string) => `\x1b[94m${value}\x1b[0m`, success: (value: string) => `\x1b[32m${value}\x1b[0m`, - fail: (value: string) => `\x1b[31m${value}\x1b[0m`, + fail: (value: string) => `\x1b[91m${value}\x1b[0m`, + bg: (bg: number, text: string) => { + const padding = ' '.repeat(1); + const paddedText = `${padding}${text}${padding}`; + + return `\x1b[${bg}m\x1b[1m${paddedText}\x1b[0m`; + }, }; export const getLargestStringLength = (arr: string[]): number => diff --git a/src/helpers/hr.ts b/src/helpers/hr.ts index bdd1fb4c..a52927ca 100644 --- a/src/helpers/hr.ts +++ b/src/helpers/hr.ts @@ -1,20 +1,8 @@ import { EOL } from 'node:os'; import process from 'node:process'; -let lastLenght: number = 0; +export const hr = () => { + const line = '⎯'.repeat(process.stdout.columns - 10); -export const hr = (size?: number) => { - const pad = 10; - const limit = process.stdout.columns - pad; - const fileLenght = typeof size === 'number' ? Math.floor(size / 2) + pad : 0; - const columns = - fileLenght > 0 && fileLenght <= limit - ? fileLenght - : lastLenght > 0 - ? lastLenght - : limit; - const line = '⎯'.repeat(columns); - lastLenght = columns; - - console.log(`${EOL}\x1b[2m${line}\x1b[0m${EOL}`); + console.log(`${EOL}\x1b[2m\x1b[90m${line}\x1b[0m${EOL}`); }; diff --git a/src/helpers/logs.ts b/src/helpers/logs.ts index 5f8a36e2..e1d53a41 100644 --- a/src/helpers/logs.ts +++ b/src/helpers/logs.ts @@ -3,8 +3,4 @@ import { Configs } from '../@types/poku.js'; export const isQuiet = (configs?: Configs): boolean => typeof configs?.quiet === 'boolean' && Boolean(configs?.quiet); -export const showSuccesses = (configs?: Configs): boolean => - Boolean(configs?.log?.success); - -export const showFailures = (configs?: Configs): boolean => - typeof configs?.log?.fail === 'undefined' || Boolean(configs?.log?.fail); +export const isDebug = (configs?: Configs): boolean => Boolean(configs?.debug); diff --git a/src/helpers/parseAsssetion.ts b/src/helpers/parseAsssetion.ts index a85a17cc..26aca330 100644 --- a/src/helpers/parseAsssetion.ts +++ b/src/helpers/parseAsssetion.ts @@ -1,4 +1,5 @@ import process from 'node:process'; +import path from 'node:path'; import assert from 'node:assert'; import { EOL } from 'node:os'; import { format } from './format.js'; @@ -33,54 +34,77 @@ const findFile = (error: Error) => { return file; }; +const formatFail = (str: string) => format.bold(format.fail(`✘ ${str}`)); + export const parseAssertion = ( cb: () => void, options: ParseAssertionOptions ) => { + const isPoku = + typeof process.env?.FILE === 'string' && process.env?.FILE.length > 0; + const FILE = process.env.FILE; + try { cb(); + + if (typeof options.message === 'string') { + const message = isPoku + ? `${format.bold(format.success(`✔ ${options.message}`))} ${format.dim(format.success(`› ${FILE}`))}` + : format.bold(format.success(`✔ ${options.message}`)); + + console.log(message); + } } catch (error) { if (error instanceof assert.AssertionError) { const { code, actual, expected, operator } = error; - const file = findFile(error); + const absoultePath = findFile(error); + const file = path.relative(path.resolve(process.cwd()), absoultePath); - hr(); + let message: string = ''; - if (typeof options.message === 'string') - console.log(format.bold(options.message), EOL); + if (typeof options.message === 'string') message = options.message; else if (options.message instanceof Error) - console.log(format.bold(options.message.message), EOL); + message = options.message.message; else if (typeof options.defaultMessage === 'string') - console.log(options.defaultMessage, EOL); + message = options.defaultMessage; + + const finalMessage = + message?.trim().length > 0 + ? `${formatFail(message)}` + : `${formatFail('No Message')}`; - console.log(format.dim('Code: '), format.bold(format.fail(code))); - file && console.log(format.dim('File: '), file); - console.log(format.dim('Operator:'), operator); + console.log( + isPoku + ? `${finalMessage} ${format.dim(format.fail(`› ${FILE}`))}` + : finalMessage + ); - hr(); + file && console.log(`${format.dim(' File')} ${file}`); + console.log(`${format.dim(' Code')} ${code}`); + console.log(`${format.dim(' Operator')} ${operator}${EOL}`); if (!options?.hideDiff) { - console.log(format.dim(`${options?.actual || 'Actual'}:`)); + console.log(format.dim(` ${options?.actual || 'Actual'}:`)); console.log( format.bold( typeof actual === 'function' || actual instanceof RegExp - ? String(actual) - : format.fail(JSON.stringify(actual)) + ? ` ${String(actual)}` + : ` ${format.fail(JSON.stringify(actual))}` ) ); console.log( - `${EOL}${format.dim(`${options?.expected || 'Expected'}:`)}` + `${EOL} ${format.dim(`${options?.expected || 'Expected'}:`)}` ); console.log( format.bold( - typeof expected === 'function' || expected instanceof RegExp - ? String(expected) - : format.success(JSON.stringify(expected)) + `${ + typeof expected === 'function' || expected instanceof RegExp + ? ` ${String(expected)}` + : ` ${format.success(JSON.stringify(expected))}` + }` ) ); - - hr(); } if (options.throw) { diff --git a/src/helpers/remove-repeats.ts b/src/helpers/remove-repeats.ts new file mode 100644 index 00000000..8e455c96 --- /dev/null +++ b/src/helpers/remove-repeats.ts @@ -0,0 +1,30 @@ +export const removeConsecutiveRepeats = ( + arr: string[], + specificItem: RegExp +): string[] => { + const result: string[] = []; + let consecutiveCount = 0; + + for (let i = 0; i < arr.length; i++) { + if (specificItem.test(arr[i])) { + consecutiveCount++; + if (consecutiveCount <= 2) { + result.push(arr[i]); + } + } else { + consecutiveCount = 0; + result.push(arr[i]); + } + + // Check if the next item is different or we're at the end of the array + if (i + 1 === arr.length || !specificItem.test(arr[i + 1])) { + // If more than two consecutive, remove them from the result + if (consecutiveCount > 2) { + result.splice(-consecutiveCount); + } + consecutiveCount = 0; // Reset the counter for the next group + } + } + + return result; +}; diff --git a/src/modules/assert.ts b/src/modules/assert.ts index a6f55800..6b0b9cf2 100644 --- a/src/modules/assert.ts +++ b/src/modules/assert.ts @@ -3,7 +3,6 @@ import { parseAssertion, ParseAssertionOptions, } from '../helpers/parseAsssetion.js'; -import { format } from '../helpers/format.js'; const ok = (value: unknown, message?: ParseAssertionOptions['message']): void => parseAssertion(() => nodeAssert.ok(value), { message }); @@ -47,7 +46,7 @@ const doesNotMatch = ( message, actual: 'Value', expected: 'RegExp', - defaultMessage: format.bold('Value should not match regExp'), + defaultMessage: 'Value should not match regExp', }); function doesNotReject( @@ -78,7 +77,7 @@ async function doesNotReject( }, { message, - defaultMessage: format.bold('Got unwanted rejection'), + defaultMessage: 'Got unwanted rejection', hideDiff: true, throw: true, } @@ -95,7 +94,7 @@ async function doesNotReject( { message: typeof errorOrMessage === 'string' ? errorOrMessage : undefined, - defaultMessage: format.bold('Got unwanted rejection'), + defaultMessage: 'Got unwanted rejection', hideDiff: true, throw: true, } @@ -132,7 +131,7 @@ function doesNotThrow( }, { message: message, - defaultMessage: format.bold('Expected function not to throw'), + defaultMessage: 'Expected function not to throw', hideDiff: true, throw: true, } @@ -149,7 +148,7 @@ function doesNotThrow( }, { message: msg, - defaultMessage: format.bold('Expected function not to throw'), + defaultMessage: 'Expected function not to throw', hideDiff: true, throw: true, } @@ -188,7 +187,7 @@ function throws( }, { message: message, - defaultMessage: format.bold('Expected function to throw'), + defaultMessage: 'Expected function to throw', hideDiff: true, } ); @@ -204,7 +203,7 @@ function throws( }, { message: msg, - defaultMessage: format.bold('Expected function to throw'), + defaultMessage: 'Expected function to throw', hideDiff: true, } ); @@ -255,7 +254,7 @@ const match = ( message, actual: 'Value', expected: 'RegExp', - defaultMessage: format.bold('Value should match regExp'), + defaultMessage: 'Value should match regExp', }); const ifError = (value: unknown): void => { @@ -267,7 +266,7 @@ const ifError = (value: unknown): void => { throw error; }, { - defaultMessage: format.bold('Expected no error, but received an error'), + defaultMessage: 'Expected no error, but received an error', hideDiff: true, throw: true, } @@ -285,7 +284,7 @@ const fail = (message?: ParseAssertionOptions['message']): void => { }, { message, - defaultMessage: format.bold('Test failed intentionally'), + defaultMessage: 'Test failed intentionally', hideDiff: true, } ); @@ -320,9 +319,8 @@ async function rejects( }, { message, - defaultMessage: format.bold( - 'Expected promise to be rejected with specified error' - ), + defaultMessage: + 'Expected promise to be rejected with specified error', hideDiff: true, } ); @@ -338,7 +336,7 @@ async function rejects( }, { message: msg, - defaultMessage: format.bold('Expected promise to be rejected'), + defaultMessage: 'Expected promise to be rejected', hideDiff: true, } ); diff --git a/src/modules/exit.ts b/src/modules/exit.ts index d297f1ef..53123ab6 100644 --- a/src/modules/exit.ts +++ b/src/modules/exit.ts @@ -1,27 +1,34 @@ import process from 'node:process'; -import { EOL } from 'node:os'; import { hr } from '../helpers/hr.js'; import { Code } from '../@types/code.js'; +import { results } from '../services/run-tests.js'; +import { format } from '../helpers/format.js'; export const exit = (code: Code, quiet?: boolean) => { + const isPoku = results.success > 0 || results.fail > 0; + !quiet && process.on('exit', (code) => { - console.log(`Exited with code`, code, EOL); + isPoku && + console.log( + format.bg(42, `PASS › ${results.success}`), + format.bg(results.fail === 0 ? 100 : 41, `FAIL › ${results.fail}`) + ); + + isPoku && hr(); + + console.log( + `${format.dim('Exited with code')} ${format.bold(format?.[code === 0 ? 'success' : 'fail'](String(code)))}` + ); }); - !quiet && hr(); + isPoku && !quiet && hr(); - if (code !== 0) { - !quiet && console.log('Some tests failed.'); - process.exit(1); - } + if (code !== 0) process.exit(1); - !quiet && console.log('All tests passed.'); process.exit(0); }; -process.stdout.on('resize', hr); - process.on('unhandledRejection', (reason) => { console.log('unhandledRejection', reason); process.exit(1); diff --git a/src/modules/list-files.ts b/src/modules/list-files.ts index 4f9dfe49..dd041963 100644 --- a/src/modules/list-files.ts +++ b/src/modules/list-files.ts @@ -33,6 +33,7 @@ export const listFiles = ( for (const file of currentFiles) { const fullPath = path.join(dirPath, file); + if (/node_modules/.test(fullPath)) continue; if (exclude && exclude.some((regex) => regex.test(fullPath))) continue; if (fs.statSync(fullPath).isDirectory()) diff --git a/src/modules/poku.ts b/src/modules/poku.ts index 57c753c3..fd10dd66 100644 --- a/src/modules/poku.ts +++ b/src/modules/poku.ts @@ -1,8 +1,14 @@ +import { EOL } from 'node:os'; import { Code } from '../@types/code.js'; import { Configs } from '../@types/poku.js'; import { forceArray } from '../helpers/force-array.js'; import { runTests, runTestsParallel } from '../services/run-tests.js'; import { exit } from './exit.js'; +import { format } from '../helpers/format.js'; +import { isQuiet } from '../helpers/logs.js'; +import { hr } from '../helpers/hr.js'; +import { fileResults } from '../services/run-test-file.js'; +import { indentation } from '../helpers/indentation.js'; export async function poku( targetDirs: string | string[], @@ -17,14 +23,41 @@ export async function poku( configs?: Configs ): Promise { let code: Code = 0; - const dirs = forceArray(targetDirs); + + const prepareDirs = forceArray(targetDirs); + const dirs = prepareDirs.length > 0 ? prepareDirs : ['./']; + const showLogs = !isQuiet(configs); if (configs?.parallel) { - const results = await Promise.all( + if (showLogs) { + hr(); + console.log(`${format.bold('Running the Test Suite in Parallel')}${EOL}`); + } + + const concurrency = await Promise.all( dirs.map((dir) => runTestsParallel(dir, configs)) ); - if (results.some((result) => !result)) code = 1; + if (concurrency.some((result) => !result)) code = 1; + + showLogs && hr(); + + if (showLogs && fileResults.success.length > 0) + console.log( + fileResults.success + .map( + (current) => + `${indentation.test}${format.success('✔')} ${format.dim(current)}` + ) + .join(EOL) + ); + + if (showLogs && fileResults.fail.length > 0) + console.log( + fileResults.fail + .map((current) => `${indentation.test}${format.fail('✘')} ${current}`) + .join(EOL) + ); if (configs?.noExit) return code; diff --git a/src/services/run-test-file.ts b/src/services/run-test-file.ts index 0d9e64a7..1cbf3d33 100644 --- a/src/services/run-test-file.ts +++ b/src/services/run-test-file.ts @@ -1,59 +1,125 @@ import process from 'node:process'; -import { spawn } from 'node:child_process'; +import { EOL } from 'node:os'; import path from 'node:path'; +import { spawn } from 'node:child_process'; import { runner } from '../helpers/runner.js'; import { indentation } from '../helpers/indentation.js'; import { format } from '../helpers/format.js'; import { Configs } from '../@types/poku.js'; -import { showFailures, showSuccesses, isQuiet } from '../helpers/logs.js'; +import { isDebug, isQuiet } from '../helpers/logs.js'; +import { removeConsecutiveRepeats } from '../helpers/remove-repeats.js'; + +type FileResults = { + success: string[]; + fail: string[]; +}; + +export const fileResults: FileResults = { + success: [], + fail: [], +}; export const runTestFile = ( filePath: string, configs?: Configs ): Promise => new Promise((resolve) => { - let output = ''; + const runtimeOptions = runner(filePath, configs); + const runtime = runtimeOptions.shift(); + const runtimeArguments = + runtimeOptions.length > 1 ? [...runtimeOptions, filePath] : [filePath]; + const fileRelative = path.relative(process.cwd(), filePath); const showLogs = !isQuiet(configs); - const showSuccess = showSuccesses(configs); - const showFailure = showFailures(configs); + const showSuccess = isDebug(configs); + const pad = configs?.parallel ? ' ' : ' '; - const log = () => console.log(output?.trim()); + let output = ''; - const fileRelative = path.relative(process.cwd(), filePath); - showLogs && - console.log(`${indentation.test}${format.info('›')} ${fileRelative}`); + const log = () => { + const outputs = removeConsecutiveRepeats( + showSuccess + ? output.split(/(\r\n|\r|\n)/) + : output.split(/(\r\n|\r|\n)/).filter((current) => { + if (current.includes('Exited with code')) return false; + return ( + /u001b\[0m|(\r\n|\r|\n)/i.test(JSON.stringify(current)) || + current === '' + ); + }), + /(\r\n|\r|\n)|^$/ + ); - const runtimeOptions = runner(filePath, configs); - const runtime = runtimeOptions.shift(); - const runtimeArguments = - runtimeOptions.length > 1 ? [...runtimeOptions, filePath] : [filePath]; + // Remove last EOL + outputs.length > 1 && outputs.pop(); + + if ( + !showSuccess && + /error:/i.test(output) && + !/error:/i.test(outputs.join()) + ) + Object.assign(outputs, [ + ...outputs, + format.bold( + format.fail(`✘ External Error ${format.dim(`› ${fileRelative}`)}`) + ), + format.dim(' For detailed diagnostics:'), + `${format.dim(` CLI ›`)} rerun with the ${format.bold('--debug')} flag enabled.`, + `${format.dim( + ` API ›` + )} set the config option ${format.bold('debug')} to true.`, + `${format.dim(' RUN ›')} ${format.bold( + `${runtime === 'tsx' ? 'npx tsx' : runtime}${runtimeArguments.slice(0, -1).join(' ')} ${fileRelative}` + )}`, + ]); + + const mappedOutputs = outputs.map((current) => `${pad}${current}`); + + if (outputs.length === 1 && outputs[0] === '') return; + + console.log( + showSuccess ? mappedOutputs.join('') : mappedOutputs.join(EOL) + ); + }; + + const stdOut = (data: Buffer): void => { + output += String(data); + }; + + if (!configs?.parallel) { + showLogs && + console.log( + `${indentation.test}${format.info(format.dim('●'))} ${format.dim(fileRelative)}` + ); + } const child = spawn(runtime!, runtimeArguments, { stdio: ['inherit', 'pipe', 'pipe'], - env: process.env, + env: { + ...process.env, + FILE: configs?.parallel ? fileRelative : '', + }, }); - child.stdout.on('data', (data) => { - output += data.toString(); - }); + child.stdout.on('data', stdOut); - child.stderr.on('data', (data) => { - output += data.toString(); - }); + child.stderr.on('data', stdOut); child.on('close', (code) => { - if ( - showLogs && - ((code === 0 && showSuccess) || (code !== 0 && showFailure)) - ) - log(); + if (showLogs) log(); - resolve(code === 0); + const result = code === 0; + + if (result) fileResults.success.push(fileRelative); + else fileResults.fail.push(fileRelative); + + resolve(result); }); child.on('error', (err) => { console.log(`Failed to start test: ${filePath}`, err); + fileResults.fail.push(fileRelative); + resolve(false); }); }); diff --git a/src/services/run-tests.ts b/src/services/run-tests.ts index bfa1c100..632d421a 100644 --- a/src/services/run-tests.ts +++ b/src/services/run-tests.ts @@ -5,11 +5,16 @@ import { runner } from '../helpers/runner.js'; import { indentation } from '../helpers/indentation.js'; import { listFiles } from '../modules/list-files.js'; import { hr } from '../helpers/hr.js'; -import { format, getLargestStringLength } from '../helpers/format.js'; +import { format } from '../helpers/format.js'; import { runTestFile } from './run-test-file.js'; import { Configs } from '../@types/poku.js'; import { isQuiet } from '../helpers/logs.js'; +export const results = { + success: 0, + fail: 0, +}; + export const runTests = async ( dir: string, configs?: Configs @@ -24,9 +29,9 @@ export const runTests = async ( let passed = true; if (showLogs) { - hr(getLargestStringLength(files)); + hr(); console.log( - `${format.bold('Directory:')} ${format.underline(currentDir)}${EOL}` + `${format.bold('Directory:')} ${format.underline(`./${currentDir}`)}${EOL}` ); } @@ -40,13 +45,21 @@ export const runTests = async ( const counter = format.counter(testNumber, totalTests); const command = `${runner(fileRelative, configs).join(' ')} ${fileRelative}`; const nextLine = i + 1 !== files.length ? EOL : ''; - const log = `${counter}/${totalTests} ${command}${nextLine}`; + const log = `${counter}/${totalTests} ${command}`; if (testPassed) { + ++results.success; + showLogs && - console.log(`${indentation.test}${format.success('✔')} ${log}`); + console.log( + `${indentation.test}${format.success('✔')} ${log}`, + nextLine + ); } else { - showLogs && console.log(`${indentation.test}${format.fail('✘')} ${log}`); + ++results.fail; + + showLogs && + console.log(`${indentation.test}${format.fail('✘')} ${log}`, nextLine); passed = false; } } @@ -61,25 +74,21 @@ export const runTestsParallel = async ( const cwd = process.cwd(); const testDir = path.join(cwd, dir); const files = listFiles(testDir, undefined, configs); - const showLogs = !isQuiet(configs); const promises = files.map(async (filePath) => { - const fileRelative = path.relative(cwd, filePath); const testPassed = await runTestFile(filePath, configs); - const command = `${runner(fileRelative).join(' ')} ${fileRelative}`; - if (testPassed) { - showLogs && - console.log(`${indentation.test}${format.success('✔')} ${command}`); - } else { - showLogs && - console.log(`${indentation.test}${format.fail('✘')} ${command}`); + if (!testPassed) { + ++results.fail; return false; } + + ++results.success; + return true; }); - const results = await Promise.all(promises); + const concurrency = await Promise.all(promises); - return results.every((result) => result); + return concurrency.every((result) => result); }; diff --git a/test/helpers/check-node.test.ts b/test/helpers/check-node.test.ts index d21d69ce..dcd8917a 100644 --- a/test/helpers/check-node.test.ts +++ b/test/helpers/check-node.test.ts @@ -15,13 +15,22 @@ export const executeDockerCompose = (serviceName: string): Promise => { ]; return new Promise((resolve, reject) => { - const downProcess = spawn(command, argsDown, { cwd, stdio: 'inherit' }); + const downProcess = spawn(command, argsDown, { cwd }); downProcess.on('close', () => { - const upProcess = spawn(command, argsUp, { cwd, stdio: 'inherit' }); + const upProcess = spawn(command, argsUp, { cwd }); upProcess.on('close', (exitCode) => { - resolve(exitCode === 0 ? 0 : 1); + if (exitCode !== 0) { + const logsProcess = spawn(command, ['logs', '-f', serviceName], { + cwd, + stdio: 'inherit', + }); + + logsProcess.on('close', () => resolve(1)); + + logsProcess.on('error', (error) => reject(error)); + } else resolve(exitCode === 0 ? 0 : 1); }); upProcess.on('error', (error) => reject(error)); diff --git a/test/integration/code.test.ts b/test/integration/code.test.ts index 8407fd9c..b6664422 100644 --- a/test/integration/code.test.ts +++ b/test/integration/code.test.ts @@ -2,33 +2,30 @@ import { poku, assert } from '../../src/index.js'; (async () => { { - // Testing all paths as a string array const code = await poku(['./test/fixtures/success', 'test/fixtures/fail'], { noExit: true, quiet: true, }); - assert.deepStrictEqual(code, 1); + assert.deepStrictEqual(code, 1, 'Testing all paths as a string array'); } - // Testing a fail path as string { const code = await poku('./test/fixtures/fail', { noExit: true, quiet: true, }); - assert.deepStrictEqual(code, 1); + assert.deepStrictEqual(code, 1, 'Testing a fail path as string'); } - // Testing a success path as string { const code = await poku('./test/fixtures/success', { noExit: true, quiet: true, }); - assert.deepStrictEqual(code, 0); + assert.deepStrictEqual(code, 0, 'Testing a success path as string'); } { @@ -40,7 +37,6 @@ import { poku, assert } from '../../src/index.js'; assert.deepStrictEqual(code, 0); } - // Only path that contains "success" { const code = await poku(['./test/fixtures/success', 'test/fixtures/fail'], { noExit: true, @@ -48,10 +44,9 @@ import { poku, assert } from '../../src/index.js'; quiet: true, }); - assert.deepStrictEqual(code, 0); + assert.deepStrictEqual(code, 0, 'Filter paths that contains "success"'); } - // Only path that contains "fail" { const code = await poku(['./test/fixtures/success', 'test/fixtures/fail'], { noExit: true, @@ -59,10 +54,9 @@ import { poku, assert } from '../../src/index.js'; quiet: true, }); - assert.deepStrictEqual(code, 1); + assert.deepStrictEqual(code, 1, 'Filter paths that contains "fail"'); } - // No files { const code = await poku(['test/fixtures/fail'], { noExit: true, @@ -70,21 +64,9 @@ import { poku, assert } from '../../src/index.js'; quiet: true, }); - assert.deepStrictEqual(code, 0); - } - - // No files - { - const code = await poku(['test/fixtures/success'], { - noExit: true, - filter: /fail/, - quiet: true, - }); - - assert.deepStrictEqual(code, 0); + assert.deepStrictEqual(code, 0, 'No files (success filter)'); } - // Filter by extension { const code = await poku(['./test/fixtures/success', 'test/fixtures/fail'], { noExit: true, @@ -92,6 +74,6 @@ import { poku, assert } from '../../src/index.js'; quiet: true, }); - assert.deepStrictEqual(code, 1); + assert.deepStrictEqual(code, 1, 'Filter by extension'); } })(); diff --git a/test/integration/import.test.ts b/test/integration/import.test.ts index 9bb88b74..67b0d58d 100644 --- a/test/integration/import.test.ts +++ b/test/integration/import.test.ts @@ -1,6 +1,6 @@ import * as index from '../../src/index.js'; -index.assert.ok(index.poku); -index.assert.ok(index.exit); -index.assert.ok(index.listFiles); -index.assert.ok(index.assert); +index.assert.ok(index.poku, 'Importing poku method'); +index.assert.ok(index.exit, 'Importing exit method'); +index.assert.ok(index.listFiles, 'Importing listFiles'); +index.assert.ok(index.assert, 'Importing assert method'); diff --git a/test/run.test.ts b/test/run.test.ts index f015de13..756bd6c6 100644 --- a/test/run.test.ts +++ b/test/run.test.ts @@ -1,5 +1,7 @@ import { poku } from '../src/index.js'; -poku(['./test/integration', './test/unit'], { +poku(['./test/unit', './test/integration'], { parallel: true, }); + +// poku(['./test/unit']); diff --git a/test/unit/assert.test.ts b/test/unit/assert.test.ts index ec8c3d33..70650fea 100644 --- a/test/unit/assert.test.ts +++ b/test/unit/assert.test.ts @@ -2,4 +2,5 @@ import { assert } from '../../src/index.js'; -assert.deepStrictEqual(1, 1); +assert(true, 'Basic Assert'); +assert.deepStrictEqual(1, 1, 'Valid deepStrictEqual'); diff --git a/test/unit/run-test-file.test.ts b/test/unit/run-test-file.test.ts index 502eafb2..fc713e0c 100644 --- a/test/unit/run-test-file.test.ts +++ b/test/unit/run-test-file.test.ts @@ -12,15 +12,14 @@ const ext = getRuntime() === 'deno' ? 'ts' : isProduction ? 'js' : 'ts'; quiet: true, }); - assert.deepStrictEqual(code, false); + assert.deepStrictEqual(code, false, 'Failure test file case'); } - // Testing a success path as string { const code = await runTestFile(`./test/fixtures/success/exit.test.${ext}`, { quiet: true, }); - assert.deepStrictEqual(code, true); + assert.deepStrictEqual(code, true, 'Success test file case'); } })(); diff --git a/test/unit/run-tests.test.ts b/test/unit/run-tests.test.ts index 4ae2848f..121e1b03 100644 --- a/test/unit/run-tests.test.ts +++ b/test/unit/run-tests.test.ts @@ -8,16 +8,15 @@ import { runTests } from '../../src/services/run-tests.js'; quiet: true, }); - assert.deepStrictEqual(code, false); + assert.deepStrictEqual(code, false, 'Failure test directory case'); } - // Testing a success path as string { const code = await runTests('./test/fixtures/success', { noExit: true, quiet: true, }); - assert.deepStrictEqual(code, true); + assert.deepStrictEqual(code, true, 'Success test directory case'); } })(); diff --git a/website/docs/comparing.mdx b/website/docs/comparing.mdx index 41c0f13d..ca252c89 100644 --- a/website/docs/comparing.mdx +++ b/website/docs/comparing.mdx @@ -37,7 +37,7 @@ assert.deepStrictEqual('1', 1, 'Number should not be a text'); ### Running tests ```bash -npx poku ./test +npx poku ```
@@ -70,6 +70,20 @@ npm i -D jest @types/jest ts-jest
+### Configuring TypeScript + +> Add in yout _tsconfig.json_ + +```json +{ + "compilerOptions": { + "esModuleInterop": true + } +} +``` + +
+ ### Configuring Jest > _jest.config.js_ @@ -112,10 +126,10 @@ npx jest ### Installation ```bash -npm i -D mocha @types/mocha chai @types/chai ts-node +npm i -D mocha @types/mocha chai @types/chai tsx ``` -> ~45M +> ~18M
@@ -125,7 +139,7 @@ npm i -D mocha @types/mocha chai @types/chai ts-node ```json { - "loader": "ts-node/esm", + "require": "tsx", "extension": ["ts"], "spec": "./test/**/*.test.ts" } diff --git a/website/docs/documentation/assert/index.mdx b/website/docs/documentation/assert/index.mdx index d62d94e6..bec1419a 100644 --- a/website/docs/documentation/assert/index.mdx +++ b/website/docs/documentation/assert/index.mdx @@ -75,7 +75,7 @@ You can follow the [**assert documentation**](https://nodejs.org/api/assert.html To use `assert` with **TypeScript**, you will need to instal **@types/node**: ```bash -npm install --save-dev @types/node +npm i -D @types/node ``` ::: diff --git a/website/docs/documentation/helpers/list-files.mdx b/website/docs/documentation/helpers/list-files.mdx index 8bd970bd..88285cfe 100644 --- a/website/docs/documentation/helpers/list-files.mdx +++ b/website/docs/documentation/helpers/list-files.mdx @@ -17,7 +17,7 @@ listFiles('some-dir'); To use `listFiles` with **TypeScript**, you will need to instal **@types/node**: ```bash -npm install --save-dev @types/node +npm i -D @types/node ``` ::: diff --git a/website/docs/documentation/poku/configs/debug.mdx b/website/docs/documentation/poku/configs/debug.mdx new file mode 100644 index 00000000..854a24b7 --- /dev/null +++ b/website/docs/documentation/poku/configs/debug.mdx @@ -0,0 +1,27 @@ +--- +sidebar_position: 6 +--- + +# `debug` + +> `poku(targetDirs: string | string[], configs?: Configs)` +> +> `debug: boolean` + +By default **Poku** doesn't shows logs that doesn't comes from **Poku**'s **`assert`**, but you can enable them: + +> _Since **1.4.0**_ + +## API (_in-code_) + +```ts +poku(['...'], { + debug: true, +}); +``` + +## CLI + +```bash +npx poku --debug ./test +``` diff --git a/website/docs/documentation/poku/configs/filter.mdx b/website/docs/documentation/poku/configs/filter.mdx index e3d82a27..9ba57d77 100644 --- a/website/docs/documentation/poku/configs/filter.mdx +++ b/website/docs/documentation/poku/configs/filter.mdx @@ -9,7 +9,7 @@ sidebar_position: 2 > `filter: RegExp` Filter by path using **Regex** to match only the files that should be performed.
-By default, **Poku** searches for _`.test.`_ files, but you can customize it using the `filter` option. +By default, **Poku** searches for _`.test.`_ and `.spec.` files, but you can customize it using the `filter` option. ## API (_in-code_) diff --git a/website/docs/documentation/poku/configs/logs/_category_.json b/website/docs/documentation/poku/configs/logs/_category_.json deleted file mode 100644 index 50ff3b29..00000000 --- a/website/docs/documentation/poku/configs/logs/_category_.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "label": "logs", - "collapsed": false, - "link": { - "type": "generated-index" - }, - "position": 6 -} diff --git a/website/docs/documentation/poku/configs/logs/success.mdx b/website/docs/documentation/poku/configs/logs/success.mdx deleted file mode 100644 index 83af63cc..00000000 --- a/website/docs/documentation/poku/configs/logs/success.mdx +++ /dev/null @@ -1,29 +0,0 @@ ---- -sidebar_position: 1 ---- - -# `success` - -> `poku(targetDirs: string | string[], configs?: Configs)` -> -> `log.success: boolean` - -By default **Poku** doesn't shows succes logs, but you can enable it: - -## API (_in-code_) - -```ts -poku(['...'], { - log: { - success: true, - }, -}); -``` - -## CLI - -> _Since **1.3.1**_ - -```bash -npx poku --log-success ./test -``` diff --git a/website/docs/documentation/poku/include-files.mdx b/website/docs/documentation/poku/include-files.mdx index b0867bb9..86947a7b 100644 --- a/website/docs/documentation/poku/include-files.mdx +++ b/website/docs/documentation/poku/include-files.mdx @@ -5,6 +5,8 @@ sidebar_position: 1 # Include Directories > `poku(targetDirs: string | string[])` +> +> By default, **Poku** searches for _`.test.`_ and `.spec.` files, but you can customize it using the [`filter`](/docs/documentation/poku/configs/filter) option. ## API (_in-code_) @@ -16,9 +18,13 @@ poku('targetDir'); poku(['targetDirA', 'targetDirB']); ``` +```ts +poku('./'); +``` + ## CLI -By setting the directories as the last argument: +By setting the directories as the **last argument**: > _Since **1.3.0**_ @@ -30,7 +36,12 @@ npx poku targetDir npx poku targetDirA,targetDirB ``` -By using `--include` option: +```bash +# Same as ./ +npx poku +``` + +By using `--include` option, you can use it in any order: ```bash npx poku --include='targetDir' @@ -39,3 +50,7 @@ npx poku --include='targetDir' ```bash npx poku --include='targetDirA,targetDirB' ``` + +```bash +npx poku --include='./' +``` diff --git a/website/docs/index.mdx b/website/docs/index.mdx index dc83f371..f794567b 100644 --- a/website/docs/index.mdx +++ b/website/docs/index.mdx @@ -51,7 +51,9 @@ Enjoying **Poku**? Consider giving him a star ⭐️ ## Why Poku? -> **Poku** starts from the premise where tests come to help, not overcomplicate: runs test files in an individual process per file, shows progress and exits 🧙🏻 +Don't worry about `describe`, `it`, `beforeEach` and everything else 🚀 + +> You don't need to learn what you already know ✨

@@ -64,7 +66,7 @@ Enjoying **Poku**? Consider giving him a star ⭐️

- And much more! + Compare Poku with the Most Popular Test Runners

@@ -76,21 +78,21 @@ Enjoying **Poku**? Consider giving him a star ⭐️ ```bash -npm install --save-dev poku +npm i -D poku ``` ```bash -npm install --save-dev poku tsx +npm i -D poku tsx ``` ```bash -bun add --dev poku +bun add -d poku ``` @@ -122,7 +124,7 @@ import { poku } from 'https://esm.sh/poku'; ```ts import { poku } from 'poku'; - await poku(['targetDirA', 'targetDirB']); + await poku(['targetDir']); ``` @@ -131,7 +133,7 @@ import { poku } from 'https://esm.sh/poku'; ```ts import { poku } from 'npm:poku'; -await poku(['targetDirA', 'targetDirB']); +await poku(['targetDir']); ``` @@ -143,21 +145,21 @@ await poku(['targetDirA', 'targetDirB']); ```bash -npx poku targetDirA,targetDirB +npx poku targetDir ``` ```bash -bun poku targetDirA,targetDirB +bun poku targetDir ``` ```bash -deno run npm:poku targetDirA,targetDirB +deno run npm:poku targetDir ``` **Poku** requires these permissions by default: diff --git a/website/docs/overview.mdx b/website/docs/overview.mdx new file mode 100644 index 00000000..46d6edcb --- /dev/null +++ b/website/docs/overview.mdx @@ -0,0 +1,47 @@ +import SEQUENTIAL from '@site/static/img/sequential.png'; +import PARALLEL from '@site/static/img/parallel.png'; +import ASSERT_POKU from '@site/static/img/assert-poku.png'; +import ASSERT_NODE from '@site/static/img/assert-node.png'; + +# Overview + +[bun-version-url]: https://github.com/oven-sh/bun +[deno-version-url]: https://github.com/denoland/deno + +| Sequential | Concurrent | +| ------------------------ | ----------------------- | +| | | + +- By default, **Poku**: + - Searches for all _`.test.`_ and `.spec.` files, but you can customize it using the option [**`filter`**](/docs/documentation/poku/configs/filter). + - Uses `sequential` mode. +- You can use concurrecy by use the flag `--parallel` for **CLI** or the option `parallel` to `true` in **API** (_in-code_) usage. + +> Follow the same idea for [**Bun**][bun-version-url] and [**Deno**][deno-version-url]. + +
+ +**Poku** also includes the `assert` method, keeping everything as it is, but providing human readability and automatic `describe` and `it`: + +> Compatible with **Node.js**, **Bun** and **Deno**. + +```ts +import { assert } from 'poku'; // Node and Bun +import { assert } from 'npm:poku'; // Deno + +const actual = '1'; + +assert(actual, 'My first assert'); +assert.deepStrictEqual(actual, 1, 'My first assert error'); +``` + +| Using `poku` | Using `node` | +| -------------------------- | -------------------------- | +| | | + +- ❌ Both cases finish with `code 1`, as expected +- 🧑🏻‍🎓 The `message` param is optional, as it's in **Node.js** +- 💚 Yes, you can use **Poku**'s `assert` running `node ./my-file.js` +- 🐷 Unlike most, **Poku** adapts to your test, not the other way around + +> [**See the complete assert's documentation**](/docs/documentation/assert). diff --git a/website/sidebars.ts b/website/sidebars.ts index 5b4bd7e3..b23d7f89 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -4,6 +4,7 @@ const sidebars: SidebarsConfig = { docs: [ 'index', 'comparing', + 'overview', { type: 'category', label: 'Documentation', diff --git a/website/src/css/home.scss b/website/src/css/home.scss index 90d5006b..fb8531b5 100644 --- a/website/src/css/home.scss +++ b/website/src/css/home.scss @@ -175,6 +175,7 @@ text-align: center; hyphens: auto; text-shadow: 1px 1px 1px #13152dab; + font-family: 'Montserrat', sans-serif; font-size: 15px; font-style: italic; opacity: 0; @@ -182,6 +183,11 @@ forwards; animation-delay: 0.75s; + code { + padding-inline: 5px; + font-weight: 600; + } + a { color: #fff; font-weight: 600; diff --git a/website/src/pages/index.tsx b/website/src/pages/index.tsx index 4d60ed64..0e9e24e8 100644 --- a/website/src/pages/index.tsx +++ b/website/src/pages/index.tsx @@ -174,22 +174,13 @@ const Home = () => {