From ca2eb388e70ebaf49c6a3d48f6a8bb316412ecc9 Mon Sep 17 00:00:00 2001 From: Oleh Babenko Date: Mon, 30 Jun 2025 11:42:01 +0300 Subject: [PATCH 01/16] Add only as an annotation for the describe/test --- README.md | 12 ++++++++++++ docs/index.md | 12 ++++++++++++ src/core/context.mjs | 24 ++++++++++++++++++++++++ src/index.d.ts | 31 +++++++++++++++++++++++++++++++ src/index.mjs | 41 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 120 insertions(+) diff --git a/README.md b/README.md index 437e5f9..f62fa45 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,8 @@ Use `expect(actual_value)` with assertions: `skip()` Declares a skipped test or test group. Test/s is/are never run. +`only()` Declares an exclusive test or test group that will be executed. If used, all other tests are skipped. + ### `Example↓` @@ -169,6 +171,16 @@ test.skip('description', () => {}) describe.skip('description', () => {}) ``` +```js +test.only('description', () => { + // Body of the only test that will be executed +}) +//or +describe.only('description', () => { + // Body of the only test group that will be executed +}) +``` + --- ## Context options diff --git a/docs/index.md b/docs/index.md index 340b166..c644327 100644 --- a/docs/index.md +++ b/docs/index.md @@ -134,6 +134,8 @@ Use `expect(actual_value)` with assertions: `skip()` Declares a skipped test or test group. Test/s is/are never run. +`only()` Declares an exclusive test or test group that will be executed. If used, all other tests are skipped. + ### `Example↓` @@ -143,6 +145,16 @@ test.skip('description', () => {}) describe.skip('description', () => {}) ``` +```js +test.only('description', () => { + // Body of the only test that will be executed +}) +//or +describe.only('description', () => { + // Body of the only test group that will be executed +}) +``` + --- ## Context options diff --git a/src/core/context.mjs b/src/core/context.mjs index 0587a4b..bde9b5a 100644 --- a/src/core/context.mjs +++ b/src/core/context.mjs @@ -59,6 +59,18 @@ export const describe = (name, optionsOrBody, body) => { } } +describe.only = (name, optionsOrBody, body) => { + const options = typeof optionsOrBody === 'object' ? optionsOrBody : {} + const actualBody = typeof optionsOrBody === 'function' ? optionsOrBody : body + const parentDescribe = currentDescribe + currentDescribe = makeDescribe(name, { ...options, focus: true }) + actualBody() + currentDescribe = { + ...parentDescribe, + children: [...parentDescribe.children, currentDescribe], + } +} + export const test = (name, optionsOrBody, body) => { const options = typeof optionsOrBody === 'object' ? optionsOrBody : {} const actualBody = typeof optionsOrBody === 'function' ? optionsOrBody : body @@ -71,6 +83,18 @@ export const test = (name, optionsOrBody, body) => { } } +test.only = (name, optionsOrBody, body) => { + const options = typeof optionsOrBody === 'object' ? optionsOrBody : {} + const actualBody = typeof optionsOrBody === 'function' ? optionsOrBody : body + currentDescribe = { + ...currentDescribe, + children: [ + ...currentDescribe.children, + { ...makeTest(name, actualBody, options.timeout, options.tags, options.retry), focus: true }, + ], + } +} + export const skip = (name) => { printSkippedMsg(name) } diff --git a/src/index.d.ts b/src/index.d.ts index a8dacb0..dca1407 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -70,6 +70,37 @@ type Options = { * @param callback A callback that is run immediately when calling test(name, optionsOrBody, callback) */ skip(name: string, optionsOrBody: {}, body: {}): void + /** + * Declares an exclusive test group. + * Only the tests in this group are run, and all other tests are skipped. + * - `describe.only(title)` + * - `describe.only(title, details, callback)` + * - `test.only(title, callback)` + * + * **Usage** + * + * ```js + * describe.only('focused group', () => { + * test('example', () => { + * // This test will run + * }); + * }); + * ``` + * or + * + * ```js + * describe('example', () => { + * test.only('focused test', () => { + * // This test will run + * }); + * }); + * ``` + * + * @param name Test title. + * @param optionsOrBody (Optional) Object with options + * @param callback A callback that is run immediately when calling test(name, optionsOrBody, callback) + */ + only(name: string, optionsOrBody?: {}, body?: {}): void; } /** * Execute before each test case. diff --git a/src/index.mjs b/src/index.mjs index f5d6227..23d650c 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -45,6 +45,28 @@ export const describe = (name, optionsOrBody, body) => */ describe.skip = (name) => core.skip(name) +/** + * Declares an exclusive test group. + * Only the tests in this group are run, and all other tests are skipped. + * - `describe.only(title)` + * - `describe.only(title, details, callback)` + * + * **Usage** + * + * ```js + * describe.only('focused group', () => { + * test('example', () => { + * // This test will run + * }); + * }); + * ``` + * + * @param name Test title. + * @param optionsOrBody (Optional) Object with options + * @param callback A callback that is run immediately when calling describe.only(name, optionsOrBody, callback) + */ +describe.only = (...args) => core.describe.only(...args) + /** * Test a specification or test-case with the given title, test options and callback fn. * @@ -85,6 +107,25 @@ export const test = (name, optionsOrBody, body) => */ test.skip = (name) => core.skip(name) +/** + * Declares an exclusive test. + * Only this test is executed, while all others are skipped. + * - `test.only(title, callback)` + * + * **Usage** + * + * ```js + * test.only('focused test', () => { + * // This test will run + * }); + * ``` + * + * @param name Test title. + * @param optionsOrBody (Optional) Object with options + * @param callback A callback that is run immediately when calling test.only(name, optionsOrBody, callback) + */ +test.only = (...args) => core.test.only(...args) + /** * Execute before each test case. * From 0196983f05be1ef62fbc491649bc1c2473d7dce0 Mon Sep 17 00:00:00 2001 From: VadimNastoyashchy Date: Mon, 30 Jun 2025 12:26:13 +0300 Subject: [PATCH 02/16] update changelog --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e1e098..41b3bcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +### Added +- Based on [Add `only` as an annotation for the `describe/test`](https://github.com/scripterio-js/scripterio/issues/14): + - Retry functionality for tests: + - `describe.only()` Declares an exclusive `describe` that will be executed. If used, all other describes are skipped. + - `test.only()` Declares an exclusive `test` that will be executed. If used, all other tests are skipped. + +- Contributors: + - [Oleh Babenko](https://github.com/OlehBabenkoo) + +### Changed +- Updated documentation ## 1.10.0 - 2025-06-22 ### Added From 1273f0e235ebb33939f7d79cc992413666457290 Mon Sep 17 00:00:00 2001 From: VadimNastoyashchy Date: Mon, 30 Jun 2025 17:35:41 +0300 Subject: [PATCH 03/16] Add tests for config.mjs --- CHANGELOG.md | 4 ++++ __tests__/config.js | 14 ++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 __tests__/config.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 41b3bcc..bdfb053 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Retry functionality for tests: - `describe.only()` Declares an exclusive `describe` that will be executed. If used, all other describes are skipped. - `test.only()` Declares an exclusive `test` that will be executed. If used, all other tests are skipped. +- Based on [Review&Add additional `unit-tests`](https://github.com/scripterio-js/scripterio/issues/58) + - Added unit tests for + -`config.mjs` - Contributors: + - [Vadym Nastoiashchyi](https://github.com/VadimNastoyashchy) - [Oleh Babenko](https://github.com/OlehBabenkoo) ### Changed diff --git a/__tests__/config.js b/__tests__/config.js new file mode 100644 index 0000000..ebbd344 --- /dev/null +++ b/__tests__/config.js @@ -0,0 +1,14 @@ +import { describe, test, expect } from '../src/index.mjs' +import { getConfig } from '../src/config/config.mjs' + +describe('Unit tests for config.mjs', () => { + test('Check getConfig() returns correct configuration', () => { + const config = getConfig() + expect(config).toBeDefined() + expect(config.folder).toBeEqual('__tests__') + expect(config.reporter).toBeEqual('html') + expect(config.file).toBeEqual('') + expect(config.retry).toBeEqual('') + expect(config.timeout).toBeEqual('') + }) +}) From 37f1e37cd539eceb21b3002d1e376be194dc1c1a Mon Sep 17 00:00:00 2001 From: VadimNastoyashchy Date: Mon, 30 Jun 2025 17:47:26 +0300 Subject: [PATCH 04/16] Rename test files --- __tests__/{assertions.js => assertions.test.js} | 0 __tests__/{config.js => config.test.js} | 0 __tests__/{request.js => request.test.js} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename __tests__/{assertions.js => assertions.test.js} (100%) rename __tests__/{config.js => config.test.js} (100%) rename __tests__/{request.js => request.test.js} (100%) diff --git a/__tests__/assertions.js b/__tests__/assertions.test.js similarity index 100% rename from __tests__/assertions.js rename to __tests__/assertions.test.js diff --git a/__tests__/config.js b/__tests__/config.test.js similarity index 100% rename from __tests__/config.js rename to __tests__/config.test.js diff --git a/__tests__/request.js b/__tests__/request.test.js similarity index 100% rename from __tests__/request.js rename to __tests__/request.test.js From 86226206c7d84fd1fa9d938ca243bc022800685c Mon Sep 17 00:00:00 2001 From: VadimNastoyashchy Date: Mon, 30 Jun 2025 18:19:31 +0300 Subject: [PATCH 05/16] Refactore time stamp --- src/core/core.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/core.mjs b/src/core/core.mjs index 3e4737e..c415c20 100644 --- a/src/core/core.mjs +++ b/src/core/core.mjs @@ -59,9 +59,9 @@ export const run = async () => { ) tags && printTags(tags) const { failures, successes } = await runParsedBlocks(tags) + const endTimeStamp = timeStamp() printFailuresMsg(failures) printTestResult(failures, successes) - const endTimeStamp = timeStamp() printExecutionTime(startTimeStamp, endTimeStamp) await getReporter(getReporterType()) process.exit(failures.length > 0 ? EXIT_CODES.failures : EXIT_CODES.ok) From f65761bc9ae35d666346acd5d76fd5fc0de524f6 Mon Sep 17 00:00:00 2001 From: Oleh Babenko Date: Mon, 30 Jun 2025 19:22:01 +0300 Subject: [PATCH 06/16] Add todo as an annotation for the describe/test --- README.md | 10 ++++ src/core/context.mjs | 71 ++++++++++++++++++------- src/index.d.ts | 94 +++++++++++++++++++++------------ src/index.mjs | 60 +++++++++++++++++++++ src/reporters/html-template.mjs | 68 ++++++++++++++++-------- 5 files changed, 228 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index f62fa45..1970348 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,8 @@ Use `expect(actual_value)` with assertions: `only()` Declares an exclusive test or test group that will be executed. If used, all other tests are skipped. +`todo()` Declares a test or test group as "to-do." The test(s) is/are marked as pending and will not be executed. Helpful for planning and organizing future tests. + ### `Example↓` @@ -181,6 +183,14 @@ describe.only('description', () => { }) ``` +```js +test.todo('description') +//or +describe.todo('description', () => { + // This test group is a placeholder and won't run +}) +``` + --- ## Context options diff --git a/src/core/context.mjs b/src/core/context.mjs index bde9b5a..8dcae73 100644 --- a/src/core/context.mjs +++ b/src/core/context.mjs @@ -25,6 +25,8 @@ export const result = { numTests: 0, numPassed: 0, numFailed: 0, + numSkipped: 0, + numTodo: 0, results: [], } @@ -59,18 +61,24 @@ export const describe = (name, optionsOrBody, body) => { } } -describe.only = (name, optionsOrBody, body) => { - const options = typeof optionsOrBody === 'object' ? optionsOrBody : {} - const actualBody = typeof optionsOrBody === 'function' ? optionsOrBody : body - const parentDescribe = currentDescribe - currentDescribe = makeDescribe(name, { ...options, focus: true }) - actualBody() - currentDescribe = { - ...parentDescribe, - children: [...parentDescribe.children, currentDescribe], +function createDescribeVariant(extra) { + return (name, optionsOrBody, body) => { + const options = typeof optionsOrBody === 'object' ? optionsOrBody : {} + const actualBody = typeof optionsOrBody === 'function' ? optionsOrBody : body + const parentDescribe = currentDescribe + currentDescribe = makeDescribe(name, { ...options, ...extra }) + // Для todo не викликаємо body + if (!extra.todo) actualBody?.() + currentDescribe = { + ...parentDescribe, + children: [...parentDescribe.children, currentDescribe], + } } } +describe.only = createDescribeVariant({ focus: true }) +describe.todo = createDescribeVariant({ todo: true }) + export const test = (name, optionsOrBody, body) => { const options = typeof optionsOrBody === 'object' ? optionsOrBody : {} const actualBody = typeof optionsOrBody === 'function' ? optionsOrBody : body @@ -83,18 +91,32 @@ export const test = (name, optionsOrBody, body) => { } } -test.only = (name, optionsOrBody, body) => { - const options = typeof optionsOrBody === 'object' ? optionsOrBody : {} - const actualBody = typeof optionsOrBody === 'function' ? optionsOrBody : body - currentDescribe = { - ...currentDescribe, - children: [ - ...currentDescribe.children, - { ...makeTest(name, actualBody, options.timeout, options.tags, options.retry), focus: true }, - ], +function createTestVariant(extra) { + return (name, optionsOrBody, body) => { + const options = typeof optionsOrBody === 'object' ? optionsOrBody : {} + const actualBody = typeof optionsOrBody === 'function' ? optionsOrBody : body + currentDescribe = { + ...currentDescribe, + children: [ + ...currentDescribe.children, + { + ...makeTest( + name, + extra.todo ? () => {} : actualBody, + options.timeout, + options.tags, + options.retry + ), + ...extra, + }, + ], + } } } +test.only = createTestVariant({ focus: true }) +test.todo = createTestVariant({ todo: true }) + export const skip = (name) => { printSkippedMsg(name) } @@ -179,7 +201,18 @@ const runTest = async (test) => { } attempts++ } - if (!passed) { + if (test.skip) { + result.numSkipped++ + console.log( + indent(applyColor(` ${currentTest.name} (SKIPPED)`)) + ) + } + if (test.todo) { + result.numTodo++ + console.log( + indent(applyColor(` ${currentTest.name} (TODO)`)) + ) + } else if (!passed) { result.numFailed++ console.log(indent(applyColor(` ${currentTest.name}`))) failures.push(currentTest) diff --git a/src/index.d.ts b/src/index.d.ts index dca1407..687abb9 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -56,8 +56,7 @@ type Options = { * }); * ``` * or - * - * * ```js + * ```js * describe('example', () => { * test.skip('skipped test', () => { * // This test will not run @@ -67,10 +66,11 @@ type Options = { * * @param name Test title. * @param optionsOrBody (Optional) Object with options - * @param callback A callback that is run immediately when calling test(name, optionsOrBody, callback) + * @param body A callback that is linked to the skipped test */ - skip(name: string, optionsOrBody: {}, body: {}): void - /** + skip(name: string, optionsOrBody?: {}, body?: {}): void + + /** * Declares an exclusive test group. * Only the tests in this group are run, and all other tests are skipped. * - `describe.only(title)` @@ -87,7 +87,6 @@ type Options = { * }); * ``` * or - * * ```js * describe('example', () => { * test.only('focused test', () => { @@ -98,9 +97,38 @@ type Options = { * * @param name Test title. * @param optionsOrBody (Optional) Object with options - * @param callback A callback that is run immediately when calling test(name, optionsOrBody, callback) + * @param body A callback that is linked to the exclusive test */ - only(name: string, optionsOrBody?: {}, body?: {}): void; + only(name: string, optionsOrBody?: {}, body?: {}): void + + /** + * Declares a test as "to-do". + * Marks the test or test group as a placeholder for future implementation, but does not execute it. + * - `test.todo(title)` + * - `describe.todo(title)` + * + * **Usage** + * + * Marking individual tests as "to-do": + * ```js + * test.todo('Test for input validation'); + * test.todo('Handle edge cases for user roles'); + * ``` + * + * Marking a test group as "to-do": + * ```js + * describe.todo('User Profile Tests', () => { + * // Placeholder for future tests + * }); + * ``` + * + * **Terminal Output** + * Tests marked as `todo` will appear in the output as pending, without causing failures. + * + * @param name Test or group title. + * @param optionsOrBody (Optional) Object with additional options + */ + todo(name: string, optionsOrBody?: {}): void } /** * Execute before each test case. @@ -333,33 +361,33 @@ export function expect(expected: any): Assertions */ export interface Response { /** Boolean indicating if the response was successful (status in the range 200-299) */ - ok: boolean; + ok: boolean /** The status code of the response (e.g., 200 for success, 404 for not found) */ - status: number; + status: number /** The status message associated with the status code */ - statusText: string; + statusText: string /** Indicates whether or not the response is the result of a redirect */ - redirected: boolean; + redirected: boolean /** The type of the response (e.g., 'basic', 'cors', 'error') */ - type: string; + type: string /** The URL of the response */ - url: string; + url: string /** The headers associated with the response */ - headers: Headers; + headers: Headers /** Indicates whether the body has been read yet */ - bodyUsed: boolean; + bodyUsed: boolean /** Returns a promise that resolves with an ArrayBuffer representation of the body */ - arrayBuffer(): Promise; + arrayBuffer(): Promise /** Returns a promise that resolves with a Blob representation of the body */ - blob(): Promise; + blob(): Promise /** Returns a promise that resolves with a FormData representation of the body */ - formData(): Promise; + formData(): Promise /** Returns a promise that resolves with the result of parsing the body text as JSON */ - json(): Promise; + json(): Promise /** Returns a promise that resolves with the body text */ - text(): Promise; + text(): Promise /** Creates a clone of the response object */ - clone(): Response; + clone(): Response } /** @@ -393,7 +421,7 @@ export interface Response { export const request: { /** * Sends a GET request to the specified URL and returns a Response object. - * + * * @param url - The URL to send the GET request to * @param config - Optional request configuration * @returns A promise that resolves to a Response object @@ -412,33 +440,33 @@ export const request: { * } * }); */ - get(url: string, config?: RequestInit): Promise; - + get(url: string, config?: RequestInit): Promise + /** * Sends a POST request with JSON body to the specified URL * @param url The URL to send the POST request to * @param config Optional request configuration */ - post(url: string, config?: RequestInit): Promise; - + post(url: string, config?: RequestInit): Promise + /** * Sends a PUT request with JSON body to the specified URL * @param url The URL to send the PUT request to * @param config Optional request configuration */ - put(url: string, config?: RequestInit): Promise; - + put(url: string, config?: RequestInit): Promise + /** * Sends a PATCH request with JSON body to the specified URL * @param url The URL to send the PATCH request to * @param config Optional request configuration */ - patch(url: string, config?: RequestInit): Promise; - + patch(url: string, config?: RequestInit): Promise + /** * Sends a DELETE request to the specified URL * @param url The URL to send the DELETE request to * @param config Optional request configuration */ - delete(url: string, config?: RequestInit): Promise; -}; + delete(url: string, config?: RequestInit): Promise +} diff --git a/src/index.mjs b/src/index.mjs index 23d650c..8900698 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -67,6 +67,38 @@ describe.skip = (name) => core.skip(name) */ describe.only = (...args) => core.describe.only(...args) +/** + * Declares a test group as "to-do". + * Marks the entire group of tests as a placeholder for future implementation but does not execute it. + * Useful for keeping track of large features or modules that require further testing. + * - `describe.todo(title)` + * + * **Usage** + * + * Marking a test group as "to-do": + * + * ```js + * describe.todo('Feature: User Authentication Tests', () => { + * // Placeholder for tests related to user authentication + * }); + * ``` + * + * Practical example: + * ```js + * describe.todo('API Endpoint Tests', () => { + * test.todo('Test GET /users endpoint'); + * test.todo('Test POST /users endpoint'); + * }); + * ``` + * + * **Terminal Output** + * When running the test suite, `describe.todo` groups and their respective `test.todo` entries will appear in the results as pending, without causing failures or executions. + * + * @param name Group title. + * @param optionsOrBody (Optional) Object with options + * @param callback (Optional) A callback function to define additional structure inside the group. + */ +describe.todo = (...args) => core.describe.todo(...args) /** * Test a specification or test-case with the given title, test options and callback fn. * @@ -126,6 +158,34 @@ test.skip = (name) => core.skip(name) */ test.only = (...args) => core.test.only(...args) +/** + * Declares a test as "to-do". + * Marks the test as a placeholder for future implementation but doesn't execute it. + * This can be useful for tracking incomplete test cases or reminders for future work. + * - `test.todo(title)` + * + * **Usage** + * + * Marking individual tests as "to-do": + * + * ```js + * test.todo('Add validation for input data'); + * test.todo('Test error handling for invalid user sessions'); + * ``` + * + * Practical example: + * ```js + * test.todo('Implement edge case handling for data overflow'); + * test.todo('Add tests for login timeout scenarios'); + * ``` + * + * **Terminal Output** + * When running the test suite, `test.todo` tests will appear in the results as pending, but they will not fail or be executed. + * + * @param name Test title. + */ +test.todo = (...args) => core.test.todo(...args) + /** * Execute before each test case. * diff --git a/src/reporters/html-template.mjs b/src/reporters/html-template.mjs index 3f1c1fb..4711c1e 100644 --- a/src/reporters/html-template.mjs +++ b/src/reporters/html-template.mjs @@ -21,7 +21,13 @@ const formatJson = (data) => { } } -export const template = ({ numTests, numPassed, numFailed, results }) => { +export const template = ({ + numTests, + numPassed, + numFailed, + numTodo, + results, +}) => { return ` @@ -115,6 +121,18 @@ export const template = ({ numTests, numPassed, numFailed, results }) => { border-radius: 4px; overflow-x: auto; } + + .test-name.todo::before { + content: '◦'; + color: #ff9800; + } + .test-name.todo { + color: #ff9800; + font-style: italic; + } + + .stat.todo { background: #fff3e0; } + .stat.todo h3, .stat.todo p { color: #ff9800; } .describe-group { margin: 0.5rem 0; padding: 0 1rem; @@ -210,6 +228,10 @@ export const template = ({ numTests, numPassed, numFailed, results }) => {

Failed

${numFailed}

+
+

Todo

+

${numTodo}

+
@@ -319,31 +341,31 @@ export const template = ({ numTests, numPassed, numFailed, results }) => { return content .map( (test) => ` -
-
- ${test.name} +
+
+ ${test.name}${test.todo ? ' (TODO)' : ''} +
+ ${ + test.errors.length + ? ` + Show error details +
+ ${test.errors + .map( + (error) => ` +
+
${stripAnsi(error.message)}
+
${stripAnsi(error.stack)}
- ${ - test.errors.length - ? ` - Show error details -
- ${test.errors - .map( - (error) => ` -
-
${stripAnsi(error.message)}
-
${stripAnsi(error.stack)}
-
- ` - ) - .join('')} + ` + ) + .join('')}
` - : '' - } + : '' + } ${test.apiDetails ? renderApiDetails(test.apiDetails) : ''}
` From acfb974fc48a4bfdf7a41f194f9533018f73658a Mon Sep 17 00:00:00 2001 From: Oleh Babenko Date: Mon, 30 Jun 2025 19:23:05 +0300 Subject: [PATCH 07/16] Update context.mjs --- src/core/context.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/context.mjs b/src/core/context.mjs index 8dcae73..3832e23 100644 --- a/src/core/context.mjs +++ b/src/core/context.mjs @@ -25,7 +25,6 @@ export const result = { numTests: 0, numPassed: 0, numFailed: 0, - numSkipped: 0, numTodo: 0, results: [], } From 8ad4b6be48d9c29ed1018c0b6f107ece11d755c4 Mon Sep 17 00:00:00 2001 From: VadimNastoyashchy Date: Tue, 1 Jul 2025 14:06:09 +0300 Subject: [PATCH 08/16] Ref. core components --- CHANGELOG.md | 4 +++- __tests__/setup.test.js | 10 ++++++++++ src/config/setup.mjs | 39 +++++++++++++++++++++++++++++++++++---- src/core/core.mjs | 34 +--------------------------------- 4 files changed, 49 insertions(+), 38 deletions(-) create mode 100644 __tests__/setup.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index bdfb053..24d4f0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `describe.only()` Declares an exclusive `describe` that will be executed. If used, all other describes are skipped. - `test.only()` Declares an exclusive `test` that will be executed. If used, all other tests are skipped. - Based on [Review&Add additional `unit-tests`](https://github.com/scripterio-js/scripterio/issues/58) - - Added unit tests for + - Added unit tests for: -`config.mjs` + -`setup.mjs` - Contributors: - [Vadym Nastoiashchyi](https://github.com/VadimNastoyashchy) @@ -21,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Updated documentation +- Refactored core components ## 1.10.0 - 2025-06-22 ### Added diff --git a/__tests__/setup.test.js b/__tests__/setup.test.js new file mode 100644 index 0000000..abf168e --- /dev/null +++ b/__tests__/setup.test.js @@ -0,0 +1,10 @@ +import { describe, test, expect } from '../src/index.mjs' +import { chooseTestFiles } from '../src/config/setup.mjs' + +describe('Unit tests for setup.mjs', async () => { + test('Check chooseTestFiles() returns correct path/s', async () => { + const filePaths = await chooseTestFiles() + expect(filePaths).toBeDefined() + expect(filePaths.length).toBeGreaterThan(1) + }) +}) diff --git a/src/config/setup.mjs b/src/config/setup.mjs index 0a05925..ed395e7 100644 --- a/src/config/setup.mjs +++ b/src/config/setup.mjs @@ -1,14 +1,28 @@ import path from 'path' import fs from 'fs' +import { getConfig } from '../config/config.mjs' -const getAllFilePaths = (dir) => { +const config = getConfig() + +const getSingleFilePath = async (dir) => { + try { + const fullPath = dir + await fs.promises.access(fullPath) + return [fullPath] + } catch { + console.error(`File ${config.file} could not be accessed.`) + process.exit(0) + } +} + +const getMultipleFilePath = (dir) => { const fileNames = fs.readdirSync(dir) let filePaths = [] fileNames.forEach((fileName) => { const filePath = path.join(dir, fileName) const stat = fs.statSync(filePath) if (stat.isDirectory()) { - filePaths = filePaths.concat(getAllFilePaths(filePath)) + filePaths = filePaths.concat(getMultipleFilePath(filePath)) } else if (fileName.endsWith('.js')) { filePaths.push(filePath) } @@ -16,6 +30,23 @@ const getAllFilePaths = (dir) => { return filePaths } -export const getMultipleFilePath = (fileDir) => { - return getAllFilePaths(fileDir) +const hasSingleFile = () => config.file + +const getTestFile = async () => { + return getSingleFilePath(path.resolve(process.cwd(), config.file)) +} + +const getTestFiles = async () => { + return getMultipleFilePath(path.resolve(process.cwd(), config.folder)) } + +export const getTags = () => { + return config.tags ? config.tags.split(',') : '' +} + +export const getReporterType = () => { + return config.reporter || '' +} + +export const chooseTestFiles = () => + hasSingleFile() ? getTestFile() : getTestFiles() diff --git a/src/core/core.mjs b/src/core/core.mjs index c415c20..7cb75e8 100644 --- a/src/core/core.mjs +++ b/src/core/core.mjs @@ -1,9 +1,7 @@ import path from 'path' -import fs from 'fs' import { applyColor, transformStackTrace } from '../utils/transform.mjs' import { runParsedBlocks } from '../core/context.mjs' -import { getConfig } from '../config/config.mjs' -import { getMultipleFilePath } from '../config/setup.mjs' +import { getTags, getReporterType, chooseTestFiles } from '../config/setup.mjs' import { timeStamp } from '../utils/support.mjs' import { EXIT_CODES } from '../core/constants.mjs' import { @@ -14,38 +12,8 @@ import { } from './output.mjs' import { getReporter } from '../reporters/index.mjs' -const config = getConfig() - Error.prepareStackTrace = transformStackTrace -const hasSingleFile = () => config.file - -const getSingleFilePath = async () => { - try { - const fullPath = path.resolve(process.cwd(), config.file) - await fs.promises.access(fullPath) - return [fullPath] - } catch { - console.error(`File ${config.file} could not be accessed.`) - process.exit(0) - } -} - -const getTestFiles = async () => { - return getMultipleFilePath(path.resolve(process.cwd(), config.folder)) -} - -const getTags = () => { - return config.tags ? config.tags.split(',') : '' -} - -const getReporterType = () => { - return config.reporter || '' -} - -const chooseTestFiles = () => - hasSingleFile() ? getSingleFilePath() : getTestFiles() - export const run = async () => { const startTimeStamp = timeStamp() const tags = getTags() From 30e9f09ed0eaa6573fb05a3d48df6082ebbd867d Mon Sep 17 00:00:00 2001 From: VadimNastoyashchy Date: Tue, 1 Jul 2025 18:25:16 +0300 Subject: [PATCH 09/16] Refactor core --- src/core/core.mjs | 40 +++------------------------------------- src/core/output.mjs | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/src/core/core.mjs b/src/core/core.mjs index 7cb75e8..9b47565 100644 --- a/src/core/core.mjs +++ b/src/core/core.mjs @@ -1,5 +1,5 @@ import path from 'path' -import { applyColor, transformStackTrace } from '../utils/transform.mjs' +import { transformStackTrace } from '../utils/transform.mjs' import { runParsedBlocks } from '../core/context.mjs' import { getTags, getReporterType, chooseTestFiles } from '../config/setup.mjs' import { timeStamp } from '../utils/support.mjs' @@ -7,8 +7,9 @@ import { EXIT_CODES } from '../core/constants.mjs' import { printExecutionTime, printRunningTestFile, - printNewLine, printTags, + printFailuresMsg, + printTestResult, } from './output.mjs' import { getReporter } from '../reporters/index.mjs' @@ -39,38 +40,3 @@ export const run = async () => { process.exit(EXIT_CODES.failures) } } - -const createFullDescription = ({ name, describeStack }) => - [...describeStack, { name }] - .map(({ name }) => `${name}`) - .join(' → ') - -const printFailureMsg = (failure) => { - console.error(applyColor(createFullDescription(failure))) - printNewLine() - failure.errors.forEach((error) => { - console.error(error.message) - console.error(error.stack) - }) - printNewLine() -} - -const printFailuresMsg = (failures) => { - if (failures.length > 0) { - printNewLine() - console.error('Failures:') - printNewLine() - } - failures.forEach(printFailureMsg) -} - -const printTestResult = (failures, successes) => { - printNewLine() - console.log( - applyColor( - `Tests: ${successes} passed, ` + - `${failures.length} failed, ` + - `${successes + failures.length} total` - ) - ) -} diff --git a/src/core/output.mjs b/src/core/output.mjs index 3725633..88bd6e6 100644 --- a/src/core/output.mjs +++ b/src/core/output.mjs @@ -53,3 +53,38 @@ export const printNewLine = () => console.log('') export const printTags = (tags) => { console.log('Using tags:' + EOL, tags) } + +const createFullDescription = ({ name, describeStack }) => + [...describeStack, { name }] + .map(({ name }) => `${name}`) + .join(' → ') + +export const printFailureMsg = (failure) => { + console.error(applyColor(createFullDescription(failure))) + printNewLine() + failure.errors.forEach((error) => { + console.error(error.message) + console.error(error.stack) + }) + printNewLine() +} + +export const printFailuresMsg = (failures) => { + if (failures.length > 0) { + printNewLine() + console.error('Failures:') + printNewLine() + } + failures.forEach(printFailureMsg) +} + +export const printTestResult = (failures, successes) => { + printNewLine() + console.log( + applyColor( + `Tests: ${successes} passed, ` + + `${failures.length} failed, ` + + `${successes + failures.length} total` + ) + ) +} From 7c279ac9c5fa934d4bf45f19ff71b8de693d467f Mon Sep 17 00:00:00 2001 From: VadimNastoyashchy Date: Wed, 2 Jul 2025 14:00:18 +0300 Subject: [PATCH 10/16] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24d4f0a..226a12a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Retry functionality for tests: - `describe.only()` Declares an exclusive `describe` that will be executed. If used, all other describes are skipped. - `test.only()` Declares an exclusive `test` that will be executed. If used, all other tests are skipped. +- Based on [Add `todo` as an annotation for the `describe/test`](https://github.com/scripterio-js/scripterio/issues/46) + - The test(s) is/are marked as pending and will not be executed. Helpful for planning and organizing future tests. + - `describe.todo()` Declares a test group as "to-do." + - `test.todo()` Declares a test as "to-do." - Based on [Review&Add additional `unit-tests`](https://github.com/scripterio-js/scripterio/issues/58) - Added unit tests for: -`config.mjs` From a5747e52a012c871ce1204e27598e2e5da00921c Mon Sep 17 00:00:00 2001 From: VadimNastoyashchy Date: Wed, 2 Jul 2025 14:20:12 +0300 Subject: [PATCH 11/16] Refactor todo --- src/core/context.mjs | 94 ++++++++++++++++++-------------------------- 1 file changed, 39 insertions(+), 55 deletions(-) diff --git a/src/core/context.mjs b/src/core/context.mjs index 3832e23..e748d00 100644 --- a/src/core/context.mjs +++ b/src/core/context.mjs @@ -48,73 +48,60 @@ const makeTest = (name, body, timeout = defaultTimeout, tags = [], retry) => ({ currentDescribe = makeDescribe('root') -export const describe = (name, optionsOrBody, body) => { +const handleDescribe = (name, optionsOrBody, body, extra = {}) => { const options = typeof optionsOrBody === 'object' ? optionsOrBody : {} const actualBody = typeof optionsOrBody === 'function' ? optionsOrBody : body const parentDescribe = currentDescribe - currentDescribe = makeDescribe(name, options) - actualBody() + currentDescribe = makeDescribe(name, { ...options, ...extra }) + if (!extra.todo) actualBody?.() currentDescribe = { ...parentDescribe, children: [...parentDescribe.children, currentDescribe], } } -function createDescribeVariant(extra) { - return (name, optionsOrBody, body) => { - const options = typeof optionsOrBody === 'object' ? optionsOrBody : {} - const actualBody = typeof optionsOrBody === 'function' ? optionsOrBody : body - const parentDescribe = currentDescribe - currentDescribe = makeDescribe(name, { ...options, ...extra }) - // Для todo не викликаємо body - if (!extra.todo) actualBody?.() - currentDescribe = { - ...parentDescribe, - children: [...parentDescribe.children, currentDescribe], - } - } +export const describe = (name, optionsOrBody, body) => { + handleDescribe(name, optionsOrBody, body) } -describe.only = createDescribeVariant({ focus: true }) -describe.todo = createDescribeVariant({ todo: true }) +describe.only = (name, optionsOrBody, body) => { + handleDescribe(name, optionsOrBody, body, { focus: true }) +} +describe.todo = (name, optionsOrBody, body) => { + handleDescribe(name, optionsOrBody, body, { todo: true }) +} -export const test = (name, optionsOrBody, body) => { +const handleTest = (name, optionsOrBody, body, extra = {}) => { const options = typeof optionsOrBody === 'object' ? optionsOrBody : {} const actualBody = typeof optionsOrBody === 'function' ? optionsOrBody : body currentDescribe = { ...currentDescribe, children: [ ...currentDescribe.children, - makeTest(name, actualBody, options.timeout, options.tags, options.retry), + { + ...makeTest( + name, + extra.todo ? () => {} : actualBody, + options.timeout, + options.tags, + options.retry + ), + ...extra, + }, ], } } -function createTestVariant(extra) { - return (name, optionsOrBody, body) => { - const options = typeof optionsOrBody === 'object' ? optionsOrBody : {} - const actualBody = typeof optionsOrBody === 'function' ? optionsOrBody : body - currentDescribe = { - ...currentDescribe, - children: [ - ...currentDescribe.children, - { - ...makeTest( - name, - extra.todo ? () => {} : actualBody, - options.timeout, - options.tags, - options.retry - ), - ...extra, - }, - ], - } - } +export const test = (name, optionsOrBody, body) => { + handleTest(name, optionsOrBody, body) } -test.only = createTestVariant({ focus: true }) -test.todo = createTestVariant({ todo: true }) +test.only = (name, optionsOrBody, body) => { + handleTest(name, optionsOrBody, body, { focus: true }) +} +test.todo = (name, optionsOrBody, body) => { + handleTest(name, optionsOrBody, body, { todo: true }) +} export const skip = (name) => { printSkippedMsg(name) @@ -175,6 +162,14 @@ const runTest = async (test) => { let passed = false global.currentTest = test currentTest.describeStack = [...describeStack] + + if (test.todo) { + result.numTodo++ + console.log( + indent(applyColor(` ${currentTest.name} (TODO)`)) + ) + } + while (attempts <= maxRetries && !passed) { if (attempts > 0) { console.log( @@ -200,18 +195,7 @@ const runTest = async (test) => { } attempts++ } - if (test.skip) { - result.numSkipped++ - console.log( - indent(applyColor(` ${currentTest.name} (SKIPPED)`)) - ) - } - if (test.todo) { - result.numTodo++ - console.log( - indent(applyColor(` ${currentTest.name} (TODO)`)) - ) - } else if (!passed) { + if (!passed) { result.numFailed++ console.log(indent(applyColor(` ${currentTest.name}`))) failures.push(currentTest) From e62cee9703d1d715235a161f6953cb401e45b0e8 Mon Sep 17 00:00:00 2001 From: VadimNastoyashchy Date: Wed, 2 Jul 2025 14:37:49 +0300 Subject: [PATCH 12/16] Add test time execution --- CHANGELOG.md | 1 + src/core/context.mjs | 5 +++ src/reporters/html-template.mjs | 55 +++++++++++++++++---------------- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 226a12a..2f4ca29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The test(s) is/are marked as pending and will not be executed. Helpful for planning and organizing future tests. - `describe.todo()` Declares a test group as "to-do." - `test.todo()` Declares a test as "to-do." +- Added test time execution to the `HTML` report: - Based on [Review&Add additional `unit-tests`](https://github.com/scripterio-js/scripterio/issues/58) - Added unit tests for: -`config.mjs` diff --git a/src/core/context.mjs b/src/core/context.mjs index e748d00..88146a6 100644 --- a/src/core/context.mjs +++ b/src/core/context.mjs @@ -9,6 +9,7 @@ import { } from '../utils/transform.mjs' import { printNewLine, printSkippedMsg } from './output.mjs' import { getConfig } from '../config/config.mjs' +import { timeStamp } from '../utils/support.mjs' const config = getConfig() const failures = [] @@ -163,6 +164,7 @@ const runTest = async (test) => { global.currentTest = test currentTest.describeStack = [...describeStack] + const startTimeStamp = timeStamp() if (test.todo) { result.numTodo++ console.log( @@ -195,6 +197,8 @@ const runTest = async (test) => { } attempts++ } + + const endTimeStamp = timeStamp() if (!passed) { result.numFailed++ console.log(indent(applyColor(` ${currentTest.name}`))) @@ -206,6 +210,7 @@ const runTest = async (test) => { } result.numTests++ result.results.push(currentTest) + currentTest.duration = endTimeStamp - startTimeStamp global.currentTest = null } diff --git a/src/reporters/html-template.mjs b/src/reporters/html-template.mjs index 4711c1e..c25085e 100644 --- a/src/reporters/html-template.mjs +++ b/src/reporters/html-template.mjs @@ -341,34 +341,37 @@ export const template = ({ return content .map( (test) => ` -
-
- ${test.name}${test.todo ? ' (TODO)' : ''} -
- ${ - test.errors.length - ? ` - Show error details -
- ${test.errors - .map( - (error) => ` -
-
${stripAnsi(error.message)}
-
${stripAnsi(error.stack)}
-
- ` - ) - .join('')} -
- ` - : '' - } - ${test.apiDetails ? renderApiDetails(test.apiDetails) : ''} +
+
+ ${test.name}${test.todo ? ' (TODO)' : ''} + ${ + typeof test.duration === 'number' ? `${test.duration} ms` : '' + } +
+ ${ + test.errors.length + ? ` + Show error details +
+ ${test.errors + .map( + (error) => ` +
+
${stripAnsi(error.message)}
+
${stripAnsi(error.stack)}
` + ) + .join('')} +
+ ` + : '' + } + ${test.apiDetails ? renderApiDetails(test.apiDetails) : ''} +
+ ` ) .join('') } From 478e5b33d4d4f18f2434dc51641c9b21691481f7 Mon Sep 17 00:00:00 2001 From: VadimNastoyashchy Date: Wed, 2 Jul 2025 14:51:43 +0300 Subject: [PATCH 13/16] Fix lint --- src/core/context.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/context.mjs b/src/core/context.mjs index 88146a6..3977520 100644 --- a/src/core/context.mjs +++ b/src/core/context.mjs @@ -198,7 +198,7 @@ const runTest = async (test) => { attempts++ } - const endTimeStamp = timeStamp() + const endTimeStamp = timeStamp() if (!passed) { result.numFailed++ console.log(indent(applyColor(` ${currentTest.name}`))) From 93c25292b6e5a72edb6132d6a021cd6791b6fa9c Mon Sep 17 00:00:00 2001 From: VadimNastoyashchy Date: Fri, 4 Jul 2025 12:48:27 +0300 Subject: [PATCH 14/16] Add HTML report filter func. --- src/reporters/html-template.mjs | 63 +++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/src/reporters/html-template.mjs b/src/reporters/html-template.mjs index c25085e..2d77e8e 100644 --- a/src/reporters/html-template.mjs +++ b/src/reporters/html-template.mjs @@ -259,12 +259,69 @@ export const template = ({ element.textContent = isShowing ? 'Show API details' : 'Hide API details'; } - document.addEventListener('DOMContentLoaded', () => { - const failedTests = document.querySelectorAll('.test-name.failed'); - failedTests.forEach(test => { + function setFilter(filter) { + document.querySelectorAll('.stat').forEach(stat => { + stat.classList.remove('active-filter'); + }); + if (filter) { + document.querySelector('.stat.' + filter).classList.add('active-filter'); + } else { + document.querySelector('.stat.total').classList.add('active-filter'); + } + + document.querySelectorAll('.test-case').forEach(tc => { + const nameDiv = tc.querySelector('.test-name'); + if (!filter || nameDiv.classList.contains(filter)) { + tc.style.display = ''; + } else { + tc.style.display = 'none'; + } + }); + + function updateDescribeVisibility(describe) { + let hasVisible = false; + const content = describe.querySelector(':scope > .describe-content'); + if (content) { + content.childNodes.forEach(child => { + if (child.nodeType !== 1) return; // skip non-elements + if (child.classList.contains('describe-group')) { + if (updateDescribeVisibility(child)) { + child.style.display = ''; + hasVisible = true; + } else { + child.style.display = 'none'; + } + } else if (child.classList.contains('test-case')) { + if (child.style.display !== 'none') { + hasVisible = true; + } + } + }); + } + describe.style.display = hasVisible ? '' : 'none'; + return hasVisible; + } + document.querySelectorAll('.describe-group').forEach(group => { + updateDescribeVisibility(group); }); + } + + document.addEventListener('DOMContentLoaded', () => { + document.querySelector('.stat.total').addEventListener('click', () => setFilter(null)); + document.querySelector('.stat.passed').addEventListener('click', () => setFilter('passed')); + document.querySelector('.stat.failed').addEventListener('click', () => setFilter('failed')); + document.querySelector('.stat.todo').addEventListener('click', () => setFilter('todo')); + document.querySelector('.stat.total').classList.add('active-filter'); }); + ` From fa67f304ef19f368d3e8bbf5f1988154f87b6efd Mon Sep 17 00:00:00 2001 From: VadimNastoyashchy Date: Fri, 4 Jul 2025 12:50:59 +0300 Subject: [PATCH 15/16] Add HTML report filter func. --- CHANGELOG.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f4ca29..e230127 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,19 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added -- Based on [Add `only` as an annotation for the `describe/test`](https://github.com/scripterio-js/scripterio/issues/14): - - Retry functionality for tests: - - `describe.only()` Declares an exclusive `describe` that will be executed. If used, all other describes are skipped. - - `test.only()` Declares an exclusive `test` that will be executed. If used, all other tests are skipped. -- Based on [Add `todo` as an annotation for the `describe/test`](https://github.com/scripterio-js/scripterio/issues/46) - - The test(s) is/are marked as pending and will not be executed. Helpful for planning and organizing future tests. - - `describe.todo()` Declares a test group as "to-do." - - `test.todo()` Declares a test as "to-do." -- Added test time execution to the `HTML` report: -- Based on [Review&Add additional `unit-tests`](https://github.com/scripterio-js/scripterio/issues/58) +- Based on [Add `only` as an annotation for `describe`/`test`](https://github.com/scripterio-js/scripterio/issues/14): + - Exclusive execution for tests: + - `describe.only()` — Runs only this `describe` block; all others are skipped. + - `test.only()` — Runs only this test; all others are skipped. +- Based on [Add `todo` as an annotation for `describe`/`test`](https://github.com/scripterio-js/scripterio/issues/46): + - Mark tests as pending to help plan and organize future work. Pending tests are not executed. + - `describe.todo()` — Marks a test group as "to-do." + - `test.todo()` — Marks a test as "to-do." +- Added test execution time to the HTML report. +- Added a filter to the HTML report for quick navigation. +- Based on [Review & Add additional unit tests](https://github.com/scripterio-js/scripterio/issues/58): - Added unit tests for: - -`config.mjs` - -`setup.mjs` + - `config.mjs` + - `setup.mjs` - Contributors: - [Vadym Nastoiashchyi](https://github.com/VadimNastoyashchy) From a1d34834c8c9c75db0d93fb1abd2f6bc00bbebf9 Mon Sep 17 00:00:00 2001 From: VadimNastoyashchy Date: Mon, 7 Jul 2025 11:33:03 +0300 Subject: [PATCH 16/16] Add todo > total tests count --- src/core/context.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/context.mjs b/src/core/context.mjs index 3977520..4626fe0 100644 --- a/src/core/context.mjs +++ b/src/core/context.mjs @@ -167,6 +167,7 @@ const runTest = async (test) => { const startTimeStamp = timeStamp() if (test.todo) { result.numTodo++ + result.numTests++ console.log( indent(applyColor(` ${currentTest.name} (TODO)`)) )