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..3832e23 100644 --- a/src/core/context.mjs +++ b/src/core/context.mjs @@ -25,6 +25,7 @@ export const result = { numTests: 0, numPassed: 0, numFailed: 0, + numTodo: 0, results: [], } @@ -59,18 +60,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 +90,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 +200,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) : ''}
`