diff --git a/CHANGELOG.md b/CHANGELOG.md index 06c046a..70a7b79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ 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 +**Features:** +- Filter tests by tags (HTML reporter). Addressed in [#66](https://github.com/scripterio-js/scripterio/issues/66). +- Assertion `toBeTypeOf()` — Check that a variable has a correct type. Addressed in [#63](https://github.com/scripterio-js/scripterio/issues/63). + +***Contributors:*** + - [Vadym Nastoiashchyi](https://github.com/VadimNastoyashchy) ## 1.11.0 - 2025-07-08 ### Added diff --git a/README.md b/README.md index 1970348..0cb0324 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,8 @@ Use `expect(actual_value)` with assertions: | `.toBeGreaterThan()` | Check actual value to be greater than expected value | | `.toBeLessThan()` | Check actual value to be less than expected value | | `.toContain()` | Use when you want to check that an item is in an array or a string. | -| `.toMatch()` | Use .toMatch() to check that a string matches a regular expression. | +| `.toMatch()` | Use to check that a string matches a regular expression. | +| `.toBeTypeOf()` | Use to check that a variable has a correct type | --- diff --git a/__tests__/assertions.test.js b/__tests__/assertions.test.js index 02fc2ae..b16b268 100644 --- a/__tests__/assertions.test.js +++ b/__tests__/assertions.test.js @@ -77,4 +77,17 @@ describe('Unit tests for assertions', () => { expect('test').toMatch('test') expect('test').toMatch(/test/i) }) + + test('Check assertion toBeType()', () => { + expect('Hello').toBeTypeOf('string') + expect(['ScripterI/O', 123]).toBeTypeOf('array') + expect(42).toBeTypeOf('number') + expect(true).toBeTypeOf('boolean') + expect({ key: 'value' }).toBeTypeOf('object') + expect(undefined).toBeTypeOf('undefined') + expect(null).toBeTypeOf('null') + expect(Symbol('sym')).toBeTypeOf('symbol') + expect(10n).toBeTypeOf('bigint') + expect(function () {}).toBeTypeOf('function') + }) }) diff --git a/assets/reporter.png b/assets/reporter.png index cecc886..4afacf6 100644 Binary files a/assets/reporter.png and b/assets/reporter.png differ diff --git a/docs/index.md b/docs/index.md index 9490e75..b582f1f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -126,7 +126,8 @@ Use `expect(actual_value)` with assertions: | `.toBeGreaterThan()` | Check actual value to be greater than expected value | | `.toBeLessThan()` | Check actual value to be less than expected value | | `.toContain()` | Use when you want to check that an item is in an array or a string. | -| `.toMatch()` | Use .toMatch() to check that a string matches a regular expression. | +| `.toMatch()` | Use to check that a string matches a regular expression. | +| `.toBeTypeOf()` | Use to check that a variable has a correct type | --- diff --git a/src/assertions/assertions.mjs b/src/assertions/assertions.mjs index aef500c..2e2fc89 100644 --- a/src/assertions/assertions.mjs +++ b/src/assertions/assertions.mjs @@ -12,3 +12,4 @@ export { toBeGreaterThan } from './toBeGreaterThan.mjs' export { toBeLessThan } from './toBeLessThan.mjs' export { toContain } from './toContain.mjs' export { toMatch } from './toMatch.mjs' +export { toBeTypeOf } from './toBeTypeOf.mjs' diff --git a/src/assertions/toBeTypeOf.mjs b/src/assertions/toBeTypeOf.mjs new file mode 100644 index 0000000..03f6dfb --- /dev/null +++ b/src/assertions/toBeTypeOf.mjs @@ -0,0 +1,173 @@ +import { AssertionError } from '../errors/assertion.mjs' +import { RunnerError } from '../errors/runner.mjs' +import { indentLine } from '../utils/transform.mjs' +import { TYPES } from '../core/constants.mjs' +import { EOL } from 'os' + +export const toBeTypeOf = (actual, expected) => { + if (typeof expected !== 'string') { + throw new RunnerError( + indentLine(`Provided type: ${typeof expected} is not a string`) + ) + } + + if (!(expected in TYPES)) { + throw new RunnerError( + indentLine(`Expect type: ${expected} is not a valid type`) + ) + } + + if (expected === 'array') { + if (!Array.isArray(actual)) { + throw new AssertionError( + indentLine('Expected: ') + + ' has an array type' + + EOL + + indentLine('Received: ') + + ` has a ${Array.isArray(actual) ? 'array' : typeof actual} type`, + { + actual, + expected, + } + ) + } + } else if (expected === 'null') { + if (actual !== null) { + throw new AssertionError( + indentLine('Expected: ') + + ' has a null type' + + EOL + + indentLine('Received: ') + + ` has a ${ + actual === undefined + ? 'undefined' + : actual === null + ? 'null' + : Array.isArray(actual) + ? 'array' + : typeof actual + } type`, + { + actual, + expected, + } + ) + } + } else if (expected === 'string') { + if (typeof actual !== 'string') { + throw new AssertionError( + indentLine('Expected: ') + + ' has a string type' + + EOL + + indentLine('Received: ') + + ` has a ${typeof actual} type`, + { + actual, + expected, + } + ) + } + } else if (expected === 'number') { + if (typeof actual !== 'number') { + throw new AssertionError( + indentLine('Expected: ') + + ' has a number type' + + EOL + + indentLine('Received: ') + + ` has a ${typeof actual} type`, + { + actual, + expected, + } + ) + } + } else if (expected === 'boolean') { + if (typeof actual !== 'boolean') { + throw new AssertionError( + indentLine('Expected: ') + + ' has a boolean type' + + EOL + + indentLine('Received: ') + + ` has a ${typeof actual} type`, + { + actual, + expected, + } + ) + } + } else if (expected === 'object') { + if ( + typeof actual !== 'object' || + actual === null || + Array.isArray(actual) + ) { + throw new AssertionError( + indentLine('Expected: ') + + ' has an object type' + + EOL + + indentLine('Received: ') + + ` has a ${typeof actual} type`, + { + actual, + expected, + } + ) + } + } else if (expected === 'function') { + if (typeof actual !== 'function') { + throw new AssertionError( + indentLine('Expected: ') + + ' has a function type' + + EOL + + indentLine('Received: ') + + ` has a ${typeof actual} type`, + { + actual, + expected, + } + ) + } + } else if (expected === 'undefined') { + if (typeof actual !== 'undefined') { + throw new AssertionError( + indentLine('Expected: ') + + ' has an undefined type' + + EOL + + indentLine('Received: ') + + ` has a ${typeof actual} type`, + { + actual, + expected, + } + ) + } + } else if (expected === 'symbol') { + if (typeof actual !== 'symbol') { + throw new AssertionError( + indentLine('Expected: ') + + ' has a symbol type' + + EOL + + indentLine('Received: ') + + ` has a ${typeof actual} type`, + { + actual, + expected, + } + ) + } + } else if (expected === 'bigint') { + if (typeof actual !== 'bigint') { + throw new AssertionError( + indentLine('Expected: ') + + ' has a bigint type' + + EOL + + indentLine('Received: ') + + ` has a ${typeof actual} type`, + { + actual, + expected, + } + ) + } + } +} diff --git a/src/core/constants.mjs b/src/core/constants.mjs index d695d19..a97977e 100644 --- a/src/core/constants.mjs +++ b/src/core/constants.mjs @@ -30,3 +30,16 @@ export const REPORTERS = { CONSOLE: 'console', HTML: 'html', } + +export const TYPES = { + bigint: 'bigint', + boolean: 'boolean', + function: 'function', + number: 'number', + object: 'object', + string: 'string', + symbol: 'symbol', + array: 'array', + null: 'null', + undefined: 'undefined', +} diff --git a/src/index.d.ts b/src/index.d.ts index 687abb9..0d2b636 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -340,6 +340,26 @@ type Assertions = { * ``` */ toMatch: (expected: any) => void + + /** + * Use .toBeTypeOf() to check that a variable has a correct type + * + * **Usage** + * + * ```js + * expect('Hello').toBeTypeOf('string') + * expect(['ScripterI/O', 123]).toBeTypeOf('array') + * expect(42).toBeTypeOf('number') + * expect(true).toBeTypeOf('boolean') + * expect({ key: 'value' }).toBeTypeOf('object') + * expect(undefined).toBeTypeOf('undefined') + * expect(null).toBeTypeOf('null') + * expect(Symbol('sym')).toBeTypeOf('symbol') + * expect(10n).toBeTypeOf('bigint') + * expect(function () {}).toBeTypeOf('function') + * ``` + */ + toBeTypeOf: (expected: any) => void } /** diff --git a/src/index.mjs b/src/index.mjs index 8900698..660140c 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -254,6 +254,7 @@ export const afterAll = (body) => core.afterAll(body) * @property {Function} toBeLessThan - Compare two numbers (received < expected). * @property {Function} toContain - Check that an item is in an array or a string contains a substring. * @property {Function} toMatch - Check that a string matches a regular expression. + * @property {Function} toBeTypeOf - Check that a variable has a correct type */ /** diff --git a/src/reporters/html-template.mjs b/src/reporters/html-template.mjs index 2d77e8e..90858a8 100644 --- a/src/reporters/html-template.mjs +++ b/src/reporters/html-template.mjs @@ -21,6 +21,25 @@ const formatJson = (data) => { } } +const extractUniqueTags = (results) => { + const tagSet = new Set() + + const extractTagsFromTests = (tests) => { + tests.forEach((test) => { + if (test.tags && Array.isArray(test.tags)) { + test.tags.forEach((tag) => { + if (tag && tag.trim()) { + tagSet.add(tag.trim()) + } + }) + } + }) + } + + extractTagsFromTests(results) + return Array.from(tagSet).sort() +} + export const template = ({ numTests, numPassed, @@ -28,6 +47,8 @@ export const template = ({ numTodo, results, }) => { + const uniqueTags = extractUniqueTags(results) + return ` @@ -204,6 +225,63 @@ export const template = ({ overflow-y: auto; white-space: pre-wrap; } + + /* Tag styles */ + .tag-filters { + margin-bottom: 1rem; + padding: 1rem; + background: #f8f9fa; + border-radius: 6px; + } + .tag-filters h3 { + margin: 0 0 0.5rem 0; + font-size: 1rem; + color: #555; + } + .tag-buttons { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + } + .tag-button { + padding: 0.25rem 0.75rem; + border: 1px solid #ddd; + border-radius: 20px; + background: white; + cursor: pointer; + font-size: 0.875rem; + transition: all 0.2s; + } + .tag-button:hover { + background: #f0f0f0; + } + .tag-button.active { + background: #1976d2; + color: white; + border-color: #1976d2; + } + .tag-button.clear { + background: #f44336; + color: white; + border-color: #f44336; + } + .tag-button.clear:hover { + background: #d32f2f; + } + .test-tags { + display: flex; + flex-wrap: wrap; + gap: 0.25rem; + margin-top: 0.5rem; + } + .test-tag { + padding: 0.125rem 0.5rem; + background: #e3f2fd; + border-radius: 12px; + font-size: 0.75rem; + color: #1976d2; + border: 1px solid #bbdefb; + } @@ -234,12 +312,37 @@ export const template = ({ + ${ + uniqueTags.length > 0 + ? ` +
+

Filter by Tags:

+
+ + ${uniqueTags + .map( + (tag) => ` + + ` + ) + .join('')} +
+
+ ` + : '' + } +
${renderDescribeGroup(groupByDescribe(results))}