diff --git a/package.json b/package.json index b52ab08..7213586 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codi-test-framework", - "version": "v0.0.40", + "version": "v0.0.41", "description": "A simple test framework for JavaScript", "main": "src/testRunner.js", "type": "module", diff --git a/src/core/describe.js b/src/core/describe.js index d052355..69579cf 100644 --- a/src/core/describe.js +++ b/src/core/describe.js @@ -12,20 +12,19 @@ import { state } from '../state/TestState.js'; export async function describe(description, callback) { const suite = { description, - tests: [], startTime: performance.now() }; - state.pushSuite(suite); + const nestedSuite = state.pushSuite(suite); try { - await Promise.resolve(callback(description)); + await Promise.resolve(callback(nestedSuite.fullPath)); } catch (error) { - console.error(chalk.red(`Suite failed: ${description}`)); + console.error(chalk.red(`Suite failed: ${nestedSuite.fullPath}`)); console.error(chalk.red(error.stack)); } finally { - suite.duration = performance.now() - suite.startTime; - state.testResults.push(suite); + nestedSuite.duration = performance.now() - nestedSuite.startTime; + state.testResults.push(nestedSuite); state.popSuite(); } } \ No newline at end of file diff --git a/src/core/it.js b/src/core/it.js index ac1fabb..d10029a 100644 --- a/src/core/it.js +++ b/src/core/it.js @@ -1,4 +1,3 @@ -import chalk from 'chalk'; import { state } from '../state/TestState.js'; /** @@ -10,12 +9,13 @@ import { state } from '../state/TestState.js'; * @returns {Promise} * @throws {Error} If called outside a describe block */ -export async function it(description, callback, suiteDescription) { - // Find the suite by its description - const suite = state.suiteStack.find(s => s.description === suiteDescription); +export async function it(description, callback, suitePath) { + const suite = suitePath + ? state.getSuiteByPath(suitePath) + : state.getCurrentSuite(); if (!suite) { - throw new Error(`Cannot find test suite with description: ${suiteDescription}`); + throw new Error('Test case defined outside of describe block'); } const test = { @@ -35,6 +35,5 @@ export async function it(description, callback, suiteDescription) { state.failedTests++; } - // Add the test to the correct suite - suite.tests.push(test); -} \ No newline at end of file + state.addTestToSuite(suite.fullPath, test); +} diff --git a/src/state/TestState.js b/src/state/TestState.js index 06d08b5..8bc54ca 100644 --- a/src/state/TestState.js +++ b/src/state/TestState.js @@ -1,31 +1,27 @@ import chalk from 'chalk'; +import EventEmitter from 'events'; + /** * Class representing the state of test execution * @class TestState */ -class TestState { - /** - * Create a test state instance - * @constructor - */ +class TestState extends EventEmitter { constructor() { + super(); /** @type {number} Number of passed tests */ this.passedTests = 0; - /** @type {number} Number of failed tests */ this.failedTests = 0; - /** @type {Array} Collection of test results */ this.testResults = []; - /** @type {Array} Stack of active test suites */ this.suiteStack = []; - /** @type {number|null} Test start time */ this.startTime = null; - /** @type {object} options */ this.options = {}; + /** @type {Map} Map of suite paths to suite objects */ + this.suiteMap = new Map(); } setOptions(options) { @@ -33,43 +29,65 @@ class TestState { ...this.options, ...options }; + this.emit('optionsUpdated', this.options); } - /** - * Reset all counters and state - * @method - */ resetCounters() { this.passedTests = 0; this.failedTests = 0; this.testResults = []; this.suiteStack = []; + this.suiteMap.clear(); + this.emit('stateReset'); } - /** - * Start the test timer - * @method - */ startTimer() { this.startTime = performance.now(); + this.emit('timerStarted', this.startTime); } - /** - * Get the total execution time - * @method - * @returns {string} Execution time in seconds - */ getExecutionTime() { return ((performance.now() - this.startTime) / 1000).toFixed(2); } /** - * Add a new suite to the stack + * Get current suite path based on stack + * @returns {string} Full path of current suite stack + */ + _getFullSuitePath() { + return this.suiteStack.map(suite => suite.description).join(' > '); + } + + /** + * Add a new suite to the stack and register it * @method * @param {object} suite - Test suite to add */ pushSuite(suite) { - this.suiteStack.push(suite); + // Get parent suite if exists + const parentSuite = this.suiteStack[this.suiteStack.length - 1]; + + // Create nested suite structure + const nestedSuite = { + ...suite, + fullPath: parentSuite + ? `${parentSuite.fullPath} > ${suite.description}` + : suite.description, + parent: parentSuite, + children: [], + tests: [] + }; + + // Add to parent's children if exists + if (parentSuite) { + parentSuite.children.push(nestedSuite); + } + + this.suiteStack.push(nestedSuite); + this.suiteMap.set(nestedSuite.fullPath, nestedSuite); + this.emit('suitePushed', nestedSuite); + + return nestedSuite; } /** @@ -78,47 +96,90 @@ class TestState { * @returns {object|undefined} Removed test suite */ popSuite() { - return this.suiteStack.pop(); + const suite = this.suiteStack.pop(); + this.emit('suitePopped', suite); + return suite; } - printSummary() { + /** + * Get current active suite + * @method + * @returns {object|undefined} Current suite + */ + getCurrentSuite() { + return this.suiteStack[this.suiteStack.length - 1]; + } + /** + * Get suite by full path + * @method + * @param {string} path - Full suite path + * @returns {object|undefined} Found suite + */ + getSuiteByPath(path) { + return this.suiteMap.get(path); + } + + /** + * Add test to a specific suite + * @method + * @param {string} suitePath - Full suite path + * @param {object} test - Test case to add + */ + addTestToSuite(suitePath, test) { + const suite = this.getSuiteByPath(suitePath); + if (!suite) { + throw new Error(`Cannot find suite: ${suitePath}`); + } + suite.tests.push(test); + this.emit('testAdded', { suite, test }); + } + + printSummary() { if (this.testResults.length > 0) { - this.testResults.forEach(suite => { + // Helper function to print suite and its children + const printSuite = (suite, indent = 0) => { + const indentation = ' '.repeat(indent); + // Print suite's tests let results = suite.tests; - - if (state.options.quiet) { + if (this.options.quiet) { results = results.filter(result => result.status === 'failed'); } + // Print suite description if (results.length > 0) { - console.log('\n' + chalk.yellow('' + chalk.bold(suite.description))); + console.log('\n' + indentation + chalk.yellow(chalk.bold(suite.description))); } - if (results.length > 0) { - results.forEach(result => { - - if (result.status === 'failed') { - console.log(chalk.red(` └─ ⛔ ${result.description} (${result.duration.toFixed(2)}ms)`)); - console.log(chalk.red(` ${result.error.message}`)); - } else { - console.log(chalk.green(` └─ ✅ ${result.description} (${result.duration.toFixed(2)}ms)`)); - } - - - }); + results.forEach(result => { + if (result.status === 'failed') { + console.log(indentation + chalk.red(` └─ ⛔ ${result.description} (${result.duration.toFixed(2)}ms)`)); + console.log(indentation + chalk.red(` ${result.error.message}`)); + } else { + console.log(indentation + chalk.green(` └─ ✅ ${result.description} (${result.duration.toFixed(2)}ms)`)); + } + }); + + // Print child suites + if (suite.children) { + suite.children.forEach(child => printSuite(child, indent + 1)); } + }; - }); + // Print only top-level suites (they will handle their children) + this.testResults + .filter(suite => !suite.parent) + .forEach(suite => printSuite(suite)); } - // Always show the final summary + console.log(chalk.bold.cyan('\nTest Summary:')); - console.log(chalk.green(` Passed: ${state.passedTests}`)); - console.log(chalk.red(` Failed: ${state.failedTests}`)); - console.log(chalk.blue(` Time: ${state.getExecutionTime()}s`)); - } + console.log(chalk.green(` Passed: ${this.passedTests}`)); + console.log(chalk.red(` Failed: ${this.failedTests}`)); + console.log(chalk.blue(` Time: ${this.getExecutionTime()}s`)); + this.emit('summaryPrinted'); + } } export const state = new TestState(); \ No newline at end of file diff --git a/src/testRunner.js b/src/testRunner.js index 2137da4..ea50be3 100755 --- a/src/testRunner.js +++ b/src/testRunner.js @@ -28,7 +28,7 @@ export const assertFalse = assertions.assertFalse; export const assertThrows = assertions.assertThrows; export const assertNoDuplicates = assertions.assertNoDuplicates; -export const version = 'v0.0.40'; +export const version = 'v0.0.41'; /** * CLI entry point for running tests diff --git a/tests/example.test.mjs b/tests/example.test.mjs index bf5d8d5..7b170c0 100644 --- a/tests/example.test.mjs +++ b/tests/example.test.mjs @@ -1,47 +1,70 @@ import { describe, it, assertEqual, assertNotEqual, assertTrue, assertFalse, assertThrows, assertNoDuplicates, runTestFunction } from '../src/testRunner.js'; // First test suite -await describe('I am an Example Test Suite', (description) => { +await describe('I am an Example Test Suite', () => { it('should pass equality assertion', () => { - assertEqual(1, 1, 'Expected 1 to equal 1'); - }, description); + assertEqual(1, 2, 'Expected 1 to equal 1'); + }); it('should pass inequality assertion', () => { assertNotEqual(1, 2, 'Expected 1 not to equal 2'); - }, description); + }); it('should pass true assertion', () => { assertTrue(true, 'Expected true to be true'); - }, description); + }); it('should pass false assertion', () => { assertFalse(false, 'Expected false to be false'); - }, description); + }); it('should pass error assertion', () => { assertThrows(() => { throw new Error('An error occurred'); }, 'An error occurred', 'Expected an error to be thrown'); - }, description); + }); it('should deeply compare objects', () => { const obj1 = { a: 1, b: { c: 2 } }; const obj2 = { a: 1, b: { c: 2 } }; assertEqual(obj1, obj2, 'Expected objects to be deeply equal'); - }, description); + }); it('should check for duplicates', () => { const array = ['field1', 'field2'] assertNoDuplicates(array, 'There should be no duplicates'); - }, description); + }); + +}); + + +await describe('I am the first describe', async () => { + await runTestFunction(testFunction, { showSummary: false }, 'I am the first function'); + await runTestFunction(testFunction, { showSummary: false }, 'I am the second function'); + + await describe('I am the second describe', async () => { + + it('should pass equality assertion', () => { + assertEqual(1, 2, 'Expected 1 to equal 1'); + }); + + it('should pass equality assertion', () => { + assertEqual(1, 2, 'Expected 1 to equal 1'); + }); + + await runTestFunction(testFunction, { showSummary: false }, 'I am the third function'); + + await describe('I am the third describe', async () => { + await runTestFunction(testFunction, { showSummary: false }, 'I am the fouth function'); + }) + }) }); -await runTestFunction(testFunction, { showSummary: false }); function testFunction() { it('First', () => { assertEqual(1, 1, 'Expected 1 to equal 1'); - }, 'Function: testFunction'); + }); } \ No newline at end of file