From ef2aa00a96aa681fe5160fc4e92435c99bd198ac Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Thu, 27 Jul 2023 05:48:41 +0200 Subject: [PATCH 1/7] Add initial assert.step, expect & async support --- shims/node/assert.js | 194 ++++++++++++++++++++++++++++++++++-------- shims/node/index.js | 195 ++++++++++++++++++++++++++++++++----------- 2 files changed, 303 insertions(+), 86 deletions(-) diff --git a/shims/node/assert.js b/shims/node/assert.js index 0a4ac0e..cc9e6ae 100644 --- a/shims/node/assert.js +++ b/shims/node/assert.js @@ -1,41 +1,145 @@ import QUnit from '../../vendor/qunit.js'; import { objectValues, objectValuesSubset, validateExpectedExceptionArgs, validateException } from '../shared/index.js'; -import assert, { AssertionError } from 'node:assert'; +import assert, { AssertionError as _AssertionError } from 'node:assert'; import util from 'node:util'; -// NOTE: Maybe do the expect, steps in some object, and also do timeout and async(?) -export default { - _steps: [], - timeout() { - return true; // NOTE: NOT implemented - }, - step(value = '') { - this._steps.push(value); - }, +export const AssertionError = _AssertionError; + +// const STEP_ERROR = { result: false, message: 'You must provide a string or object to assert.step()' }; + +// More: contexts needed for timeout +// NOTE: QUnit API provides assert on hooks, which makes it hard to make it concurrent +// NOTE: Another approach for a global report Make this._assertions.set(this.currentTest, (this._assertions.get(this.currentTest) || 0) + 1); for pushResult +// NOTE: This should *always* be a singleton(?), passed around as an argument for hooks. Seems difficult with concurrency. Singleton needs to be a concurrent data structure. + +export default class Assert { + AssertionError = _AssertionError; + + #asyncOps = []; + + constructor(module, test) { + this.module = module; + this.test = test; + } + _incrementAssertionCount() { + if (this.test) { + this.test.totalExecutedAssertions++; + } else { + this.module.tests.forEach(test => { + test.totalExecutedAssertions++; + }); + } + } + // _applyToContext(func) { + // if (this.test) { + // return func(this.test); + // } else { + // return this.module.tests.map(func); + // } + // }, + timeout(number) { + if (!Number.isInteger(number) || number < 0) { + throw new Error('assert.timeout() expects a positive integer.'); + } + + if (this.test) { + this.test.timeout = number; + } else { + this.module.tests.forEach(test => { + test.timeout = number; + }); + } + } + step(message) { + let assertionMessage = message; + let result = !!message; + + if (this.test) { + this.test.steps.push(message); + } else { + this.module.tests.forEach(test => { + test.steps.push(message); + }); + } + + if (typeof message === 'undefined' || message === '') { + assertionMessage = 'You must provide a message to assert.step'; + } else if (typeof message !== 'string') { + assertionMessage = 'You must provide a string value to assert.step'; + result = false; + } + + this.pushResult({ + result, + message: assertionMessage + }); + } verifySteps(steps, message = 'Verify steps failed!') { - const result = this.deepEqual(this._steps, steps, message); + // const actualStepsClone = this.module.test.steps.slice(); // TODO: is this needed(?) - this._steps.length = 0; + if (this.test) { + let result = this.deepEqual(this.test.steps, steps, message); + this.test.steps.length = 0; + } else { + this.module.tests.forEach(test => { + let result = this.deepEqual(test.steps, steps, message); + test.steps.length = 0; + }); + } + } + expect(number) { + if (!Number.isInteger(number) || number < 0) { + throw new Error('assert.expect() expects a positive integer.'); + } - return result; - }, - expect() { - return () => {}; // NOTE: NOT implemented - }, + if (this.test) { + this.test.expectedAssertionCount = number; + } else { + this.module.tests.forEach(test => { + test.expectedAssertionCount = number; + }); + } + } async() { - return () => {}; // NOTE: noop, node should have sanitizeResources - }, + let resolveFn; + let done = new Promise(resolve => { resolveFn = resolve; }); + this.#asyncOps.push(done); + return () => { resolveFn(); }; + + // let resolve; + // const promise = new Promise((_resolve) => { + // resolve = _resolve; + // }); + // const doneFn = () => { + // resolve(); + // }; + // if (this.test) { + // this.test.asyncOp = promise; + // } else { + // this.module.tests.forEach(test => { + // test.asyncOp = promise; + // }); + // } + // return doneFn; + } + async waitForAsyncOps() { + return Promise.all(this.#asyncOps); + } pushResult(resultInfo = {}) { - if (!result) { + this._incrementAssertionCount(); + if (!resultInfo.result) { throw new AssertionError({ actual: resultInfo.actual, expected: resultInfo.expected, - message: result.Infomessage || 'Custom assertion failed!', + message: resultInfo.message || 'Custom assertion failed!', stackStartFn: this.pushResult, }); } - }, + + return this; + } ok(state, message) { + this._incrementAssertionCount(); if (!state) { throw new AssertionError({ actual: state, @@ -44,8 +148,9 @@ export default { stackStartFn: this.ok, }); } - }, + } notOk(state, message) { + this._incrementAssertionCount(); if (state) { throw new AssertionError({ actual: state, @@ -54,8 +159,9 @@ export default { stackStartFn: this.notOk, }); } - }, + } true(state, message) { + this._incrementAssertionCount(); if (state !== true) { throw new AssertionError({ actual: state, @@ -64,8 +170,9 @@ export default { stackStartFn: this.true, }); } - }, + } false(state, message) { + this._incrementAssertionCount(); if (state !== false) { throw new AssertionError({ actual: state, @@ -74,8 +181,9 @@ export default { stackStartFn: this.false, }); } - }, + } equal(actual, expected, message) { + this._incrementAssertionCount(); if (actual != expected) { throw new AssertionError({ actual, @@ -85,8 +193,9 @@ export default { stackStartFn: this.equal, }); } - }, + } notEqual(actual, expected, message) { + this._incrementAssertionCount(); if (actual == expected) { throw new AssertionError({ actual, @@ -96,8 +205,9 @@ export default { stackStartFn: this.notEqual, }); } - }, + } propEqual(actual, expected, message) { + this._incrementAssertionCount(); let targetActual = objectValues(actual); let targetExpected = objectValues(expected); if (!QUnit.equiv(targetActual, targetExpected)) { @@ -108,8 +218,9 @@ export default { stackStartFn: this.propEqual, }); } - }, + } notPropEqual(actual, expected, message) { + this._incrementAssertionCount(); let targetActual = objectValues(actual); let targetExpected = objectValues(expected); if (QUnit.equiv(targetActual, targetExpected)) { @@ -120,8 +231,9 @@ export default { stackStartFn: this.notPropEqual, }); } - }, + } propContains(actual, expected, message) { + this._incrementAssertionCount(); let targetActual = objectValuesSubset(actual, expected); let targetExpected = objectValues(expected, false); if (!QUnit.equiv(targetActual, targetExpected)) { @@ -132,8 +244,9 @@ export default { stackStartFn: this.propContains, }); } - }, + } notPropContains(actual, expected, message) { + this._incrementAssertionCount(); let targetActual = objectValuesSubset(actual, expected); let targetExpected = objectValues(expected); if (QUnit.equiv(targetActual, targetExpected)) { @@ -144,8 +257,9 @@ export default { stackStartFn: this.notPropContains, }); } - }, + } deepEqual(actual, expected, message) { + this._incrementAssertionCount(); if (!QUnit.equiv(actual, expected)) { throw new AssertionError({ actual, @@ -155,8 +269,9 @@ export default { stackStartFn: this.deepEqual, }); } - }, + } notDeepEqual(actual, expected, message) { + this._incrementAssertionCount(); if (QUnit.equiv(actual, expected)) { throw new AssertionError({ actual, @@ -166,8 +281,9 @@ export default { stackStartFn: this.notDeepEqual, }); } - }, + } strictEqual(actual, expected, message) { + this._incrementAssertionCount(); if (actual !== expected) { throw new AssertionError({ actual, @@ -177,8 +293,9 @@ export default { stackStartFn: this.strictEqual, }); } - }, + } notStrictEqual(actual, expected, message) { + this._incrementAssertionCount(); if (actual === expected) { throw new AssertionError({ actual, @@ -188,8 +305,10 @@ export default { stackStartFn: this.notStrictEqual, }); } - }, + } throws(blockFn, expectedInput, assertionMessage) { + // TODO: This probably happens on increse shit + this?._incrementAssertionCount(); let [expected, message] = validateExpectedExceptionArgs(expectedInput, assertionMessage, 'rejects'); if (typeof blockFn !== 'function') { throw new AssertionError({ @@ -222,8 +341,9 @@ export default { message: 'Function passed to `assert.throws` did not throw an exception!', stackStartFn: this.throws, }); - }, + } async rejects(promise, expectedInput, assertionMessage) { + this._incrementAssertionCount(); let [expected, message] = validateExpectedExceptionArgs(expectedInput, assertionMessage, 'rejects'); let then = promise && promise.then; if (typeof then !== 'function') { diff --git a/shims/node/index.js b/shims/node/index.js index 52f71bc..78427a0 100644 --- a/shims/node/index.js +++ b/shims/node/index.js @@ -1,24 +1,161 @@ -import { run, describe, it, before, after, beforeEach, afterEach } from 'node:test'; -import assert from './assert.js'; +import { run, describe, it, before as _before, after as _after, beforeEach as _beforeEach, afterEach as _afterEach } from 'node:test'; +import Assert, { AssertionError } from './assert.js'; + +class TestContext { + assert; + name; + timeout; + steps = []; + expectedAssertionCount; + totalExecutedAssertions = 0; + + constructor(name, moduleContext) { + this.name = `${ModuleContext.moduleChain.map((module) => module.name).join(' | ')}}${name}`; + this.module = moduleContext; + this.module.tests.push(this); + this.assert = new Assert(moduleContext, this); + } + + complete() { + // TODO: below should only work if moduleChain is one level deep + // if (this.totalExecutedAssertions === 0) { + // this.assert.pushResult({ + // result: false, + // actual: this.totalExecutedAssertions, + // expected: '> 0', + // message: `Expected at least one assertion to be run for test: ${this.name}`, + // }); + // } else + + if (this.steps.length > 0) { + this.assert.pushResult({ + result: false, + actual: this.steps, + expected: [], + message: `Expected assert.verifySteps() to be called before end of test after using assert.step(). Unverified steps: ${this.steps.join(', ')}` + }); + } + + // TODO: how to build this for nested modules(?) + // if (this.expectedAssertionCount && this.expectedAssertionCount !== this.totalExecutedAssertions) { + // this.assert.pushResult({ + // result: false, + // actual: this.totalExecutedAssertions, + // expected: this.expectedAssertionCount, + // message: `Expected ${this.expectedAssertionCount} assertions, but ${this.totalExecutedAssertions} were run for test: ${this.name}` + // }); + // } + } +} + +class ModuleContext { + static moduleChain = []; + + static get current() { + return this.moduleChain[this.moduleChain.length - 1] || null; + } + + #tests = []; + get tests() { + return this.#tests; + } + + constructor(name) { + this.name = name; + ModuleContext.moduleChain.push(this); + } +} export const module = async function(moduleName, runtimeOptions, moduleContent) { let targetRuntimeOptions = moduleContent ? runtimeOptions : {}; let targetModuleContent = moduleContent ? moduleContent : runtimeOptions; - return describe(moduleName, assignDefaultValues(targetRuntimeOptions, { concurrency: true }), async function() { - return await targetModuleContent({ before, after, beforeEach, afterEach }, { - moduleName, - options: runtimeOptions - }); + const moduleContext = new ModuleContext(moduleName); + + return describe(moduleName, assignDefaultValues(targetRuntimeOptions, { concurrency: true }), async function () { + let afterHooks = []; + let assert; + let userProvidedModuleContent = targetModuleContent({ + _context: moduleContext, + before(beforeFn) { + return _before(async function () { + assert = assert || new Assert(moduleContext); + return await beforeFn.call(moduleContext, assert); + }); + }, + beforeEach(beforeEachFn) { + let i = 0; + return _beforeEach(async function () { + return await beforeEachFn.call(moduleContext, moduleContext.tests[i++].assert); + }); + }, + afterEach(afterEachFn) { + let i = 0; + return _afterEach(async function () { + return await afterEachFn.call(moduleContext, moduleContext.tests[i++].assert); + }); + }, + after(afterFn) { + assert = assert || new Assert(moduleContext); + afterHooks.push(afterFn); // Save user-provided hooks + + return _after(async function () { + for (const assert of moduleContext.tests.map(testContext => testContext.assert)) { + await assert.waitForAsyncOps(); + } + + for (let hook of afterHooks) { + let result = await hook.call(moduleContext, assert); + } + + moduleContext.tests.forEach(testContext => testContext.complete()); + }); + } + }, { moduleName, options: runtimeOptions }); + + ModuleContext.moduleChain.pop(); + + let result = await userProvidedModuleContent; + + if (afterHooks.length === 0) { + await _after(async () => { + for (const assert of moduleContext.tests.map(testContext => testContext.assert)) { + await assert.waitForAsyncOps(); + } + + return moduleContext.tests.forEach(testContext => testContext.complete()); + }); + } + + return result; }); } export const test = async function(testName, runtimeOptions, testContent) { + const moduleContext = ModuleContext.current; + if (!moduleContext) { + throw new Error(`Test '${testName}' called outside of module context.`); + } + let targetRuntimeOptions = testContent ? runtimeOptions : {}; let targetTestContent = testContent ? testContent : runtimeOptions; - return it(testName, assignDefaultValues(targetRuntimeOptions, { concurrency: true }), async function() { - return await targetTestContent(assert, { testName, options: runtimeOptions }); + const testContext = new TestContext(testName, moduleContext); + + return it(testName, assignDefaultValues(targetRuntimeOptions, { concurrency: true }), async function () { + let result; + try { + result = await targetTestContent.call(moduleContext, testContext.assert, { testName, options: runtimeOptions }); + + await testContext.assert.waitForAsyncOps(); + } catch (error) { + throw error; + } finally { + // testContext.complete(); + } + + console.log('test() call finish'); + return result; }); } @@ -33,43 +170,3 @@ function assignDefaultValues(options, defaultValues) { } export default { module, test, config: {} }; - -// NOTE: later maybe expose these as well: - -// import QUnit from './vendor/qunit.js'; - -// QUnit.config.autostart = false; - -// export const isLocal = QUnit.isLocal; -// export const on = QUnit.on; -// export const test = QUnit.test; -// export const skip = QUnit.skip; -// export const start = QUnit.start; -// export const is = QUnit.is; -// export const extend = QUnit.extend; -// export const stack = QUnit.stack; -// export const onUnhandledRejection = QUnit.onUnhandledRejection; -// export const assert = QUnit.assert; -// export const dump = QUnit.dump; -// export const done = QUnit.done; -// export const testStart = QUnit.testStart; -// export const moduleStart = QUnit.moduleStart; -// export const version = QUnit.version; -// export const module = QUnit.module; -// export const todo = QUnit.todo; -// export const only = QUnit.only; -// export const config = QUnit.config; -// export const objectType = QUnit.objectType; -// export const load = QUnit.load; -// export const onError = QUnit.onError; -// export const pushFailure = QUnit.pushFailure; -// export const equiv = QUnit.equiv; -// export const begin = QUnit.begin; -// export const log = QUnit.log; -// export const testDone = QUnit.testDone; -// export const moduleDone = QUnit.moduleDone; -// export const diff = QUnit.diff; - -// export default Object.assign(QUnit, { -// QUnitxVersion: '0.0.1' -// }); From 95bab0708e3a68904ab78210e5ede509191597cd Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Thu, 27 Jul 2023 05:55:24 +0200 Subject: [PATCH 2/7] Tests for stateful assertions --- test/equal-test.js | 44 +++-- test/expect-test.js | 13 ++ test/hooks-test.js | 383 +++++++++++++++++++++++++++++++++++++++ test/index.js | 5 + test/propEqual-test.js | 81 +++++---- test/step-test.js | 204 +++++++++------------ test/strictEqual-test.js | 35 ++-- test/throws-test.js | 108 +++++++++-- test/truthy-test.js | 47 +++-- 9 files changed, 704 insertions(+), 216 deletions(-) create mode 100644 test/expect-test.js create mode 100644 test/hooks-test.js diff --git a/test/equal-test.js b/test/equal-test.js index 72b0b96..181dc55 100644 --- a/test/equal-test.js +++ b/test/equal-test.js @@ -17,19 +17,33 @@ module('Assertion: Equality - passing assertions', function () { }); }); -// module('Assertion: Equality - failing assertions', function () { -// test('assert.equal', function (assert) { -// assert.equal(1, 2); -// assert.equal('foo', 'bar'); -// assert.equal({}, {}); -// assert.equal([], []); -// }); +module('Assertion: Equality - failing assertions', function (hooks) { + hooks.beforeEach(function (assert) { + let originalPushResult = assert.pushResult; + assert.pushResult = function (resultInfo) { + // Inverts the result so we can test failing assertions + resultInfo.result = !resultInfo.result; + originalPushResult.call(this, resultInfo); + }; + }); + + test('assert.equal', function (assert) { + let { throws } = assert; + + debugger; + throws(() => assert.equal(1, 2)); + throws(() => assert.equal('foo', 'bar')); + throws(() => assert.equal({}, {})); + throws(() => assert.equal([], [])); + }); -// test('assert.notEqual', function (assert) { -// assert.notEqual(1, 1); -// assert.notEqual('foo', 'foo'); -// assert.notEqual('foo', ['foo']); -// assert.notEqual('foo', { toString: function () { return 'foo'; } }); -// assert.notEqual(0, [0]); -// }); -// }); + test('assert.notEqual', function (assert) { + let { throws } = assert; + + throws(() => assert.notEqual(1, 1)); + throws(() => assert.notEqual('foo', 'foo')); + throws(() => assert.notEqual('foo', ['foo'])); + throws(() => assert.notEqual('foo', { toString: function () { return 'foo'; } })); + throws(() => assert.notEqual(0, [0])); + }); +}); diff --git a/test/expect-test.js b/test/expect-test.js new file mode 100644 index 0000000..4aaeb81 --- /dev/null +++ b/test/expect-test.js @@ -0,0 +1,13 @@ +import { module, test } from 'qunitx'; + +module('Assertion: Expect | Passing Assertions', function (hooks) { + test('expect(4) makes the test passes when there are 4 cases', function (assert) { + assert.expect(4); + + assert.ok(true); + assert.ok(true); + assert.ok(true); + assert.ok(true); + }); +}); + diff --git a/test/hooks-test.js b/test/hooks-test.js new file mode 100644 index 0000000..395adc5 --- /dev/null +++ b/test/hooks-test.js @@ -0,0 +1,383 @@ +import { module, test } from 'qunitx'; + +module('module', function () { + module('before/beforeEach/afterEach/after', function(hooks) { + hooks.before(function (assert) { + this.lastHook = 'module-before'; + }); + hooks.beforeEach(function (assert) { + assert.strictEqual(this.lastHook, 'module-before', + "Module's beforeEach runs after before"); + this.lastHook = 'module-beforeEach'; + }); + hooks.afterEach(function (assert) { + assert.strictEqual(this.lastHook, 'test-block', + "Module's afterEach runs after current test block"); + this.lastHook = 'module-afterEach'; + }); + hooks.after(function (assert) { + assert.strictEqual(this.lastHook, 'module-afterEach', + "Module's afterEach runs before after"); + this.lastHook = 'module-after'; + }); + + test('hooks order', function (assert) { + assert.expect(4); + + assert.strictEqual(this.lastHook, 'module-beforeEach', + "Module's beforeEach runs before current test block"); + this.lastHook = 'test-block'; + }); + }); + + module('modules with async hooks', hooks => { + hooks.before(async assert => { assert.step('before'); }); + hooks.beforeEach(async assert => { assert.step('beforeEach'); }); + hooks.afterEach(async assert => { assert.step('afterEach'); }); + + hooks.after(assert => { + assert.verifySteps([ + 'before', + 'beforeEach', + 'afterEach' + ]); + }); + + test('all hooks', assert => { + assert.expect(4); + }); + }); + + module('before', function (hooks) { + hooks.before(function (assert) { + assert.true(true, 'before hook ran'); + + if (typeof this.beforeCount === 'undefined') { + this.beforeCount = 0; + } + + this.beforeCount++; + }); + + test('runs before first test', function (assert) { + assert.expect(2); + assert.equal(this.beforeCount, 1, 'beforeCount should be one'); + }); + }); + + module('Test context object', function (hooks) { + hooks.beforeEach(function (assert) { + this.name = 'Test context object'; + var key; + var keys = []; + + for (key in this) { + keys.push(key); + } + assert.deepEqual(keys, ['name']); + }); + + test('keys', function (assert) { + assert.expect(1); + this.contextTest = true; + }); + }); + + module('afterEach and assert.async', function (hooks) { + hooks.beforeEach(function() { + this.state = false; + }); + hooks.afterEach(function (assert) { + assert.strictEqual(this.state, true, 'Test afterEach.'); + }); + + test('afterEach must be called after test ended', function (assert) { + var testContext = this; + var done = assert.async(); + assert.expect(1); + setTimeout(function () { + testContext.state = true; + done(); + }); + }); + }); + + + module('async beforeEach test', function (hooks) { + hooks.beforeEach(function (assert) { + var done = assert.async(); + setTimeout(function () { + assert.true(true); + done(); + }); + }); + + test('module with async beforeEach', function (assert) { + assert.expect(2); + assert.true(true); + }); + }); + + module('async afterEach test', function (hooks) { + hooks.afterEach(function (assert) { + var done = assert.async(); + setTimeout(function () { + assert.true(true); + done(); + }); + }); + + test('module with async afterEach', function (assert) { + assert.expect(2); + assert.true(true); + }); + }); + + module('save scope', function (hooks) { + hooks.before(function(assert) { + this.foo = 'bar'; + }); + hooks.beforeEach(function (assert) { + assert.equal(this.foo, 'bar'); + this.foo = 'bar'; + }); + hooks.afterEach(function (assert) { + assert.deepEqual(this.foo, 'foobar'); + }); + + test('scope check', function (assert) { + assert.expect(3); + assert.deepEqual(this.foo, 'bar'); + this.foo = 'foobar'; + }); + }); + + // module('nested modules', function () { + // module('first outer', function (hooks) { + // hooks.afterEach(function (assert) { + // assert.true(true, 'first outer module afterEach called'); + // }); + // hooks.beforeEach(function (assert) { + // assert.true(true, 'first outer beforeEach called'); + // }); + + // module('first inner', function (hooks) { + // hooks.afterEach(function (assert) { + // assert.true(true, 'first inner module afterEach called'); + // }); + // hooks.beforeEach(function (assert) { + // assert.true(true, 'first inner module beforeEach called'); + // }); + + // test('in module, before-/afterEach called in out-in-out order', function (assert) { + // var module = assert.test.module; + // assert.equal(module.name, + // 'module > nested modules > first outer > first inner'); + // // assert.expect(5); + // }); + + // // test('test after nested module is processed', function (assert) { + // // var module = assert.test.module; + // // assert.equal(module.name, 'module > nested modules > first outer'); + // // assert.expect(3); + // // }); + // }); + + // // module('second inner', function () { + // // test('test after non-nesting module declared', function (assert) { + // // var module = assert.test.module; + // // assert.equal(module.name, 'module > nested modules > first outer > second inner'); + // // assert.expect(3); + // // }); + // // }); + // }); + + // // module('second outer', function () { + // // test('test after all nesting modules processed and new module declared', function (assert) { + // // var module = assert.test.module; + // // assert.equal(module.name, 'module > nested modules > second outer'); + // // }); + // // }); + // }); + + // test('modules with nested functions does not spread beyond', function (assert) { + // assert.equal(assert.test.module.name, 'module'); + // }); + + // TODO: This is the test case for deep down tests that needs to be fixed!: + // module('contained suite arguments', function (hooks) { + // test('hook functions', function (assert) { + // assert.strictEqual(typeof hooks.beforeEach, 'function'); + // assert.strictEqual(typeof hooks.afterEach, 'function'); + // }); + + // module('outer hooks', function (hooks) { + // hooks.beforeEach(function (assert) { + // assert.true(true, 'beforeEach called'); + // }); + + // hooks.afterEach(function (assert) { + // assert.true(true, 'afterEach called'); + // }); + + // test('call hooks', function (assert) { + // assert.expect(2); + // }); + + // module('stacked inner hooks', function (hooks) { + // hooks.beforeEach(function (assert) { + // assert.true(true, 'nested beforeEach called'); + // }); + + // hooks.afterEach(function (assert) { + // assert.true(true, 'nested afterEach called'); + // }); + + // test('call hooks', function (assert) { + // assert.expect(4); + // }); + // }); + // }); + // }); + + // module('contained suite `this`', function (hooks) { + // this.outer = 1; + + // hooks.beforeEach(function () { + // this.outer++; + // }); + + // hooks.afterEach(function (assert) { + // assert.equal( + // this.outer, 42, + // 'in-test environment modifications are visible by afterEach callbacks' + // ); + // }); + + // test('`this` is shared from modules to the tests', function (assert) { + // assert.equal(this.outer, 2); + // this.outer = 42; + // }); + + // test("sibling tests don't share environments", function (assert) { + // assert.equal(this.outer, 2); + // this.outer = 42; + // }); + + // module('nested suite `this`', function (hooks) { + // this.inner = true; + + // hooks.beforeEach(function (assert) { + // assert.strictEqual(this.outer, 2); + // assert.true(this.inner); + // }); + + // hooks.afterEach(function (assert) { + // assert.strictEqual(this.outer, 2); + // assert.true(this.inner); + + // // This change affects the outermodule afterEach assertion. + // this.outer = 42; + // }); + + // test('inner modules share outer environments', function (assert) { + // assert.strictEqual(this.outer, 2); + // assert.true(this.inner); + // }); + // }); + + // test("tests can't see environments from nested modules", function (assert) { + // assert.strictEqual(this.inner, undefined); + // this.outer = 42; + // }); + // }); + + // module('nested modules before/after', { + // before: function (assert) { + // assert.true(true, 'before hook ran'); + // this.lastHook = 'before'; + // }, + // after: function (assert) { + // assert.strictEqual(this.lastHook, 'outer-after'); + // } + // }, function () { + // test('should run before', function (assert) { + // assert.expect(2); + // assert.strictEqual(this.lastHook, 'before'); + // }); + + // module('outer', { + // before: function (assert) { + // assert.true(true, 'outer before hook ran'); + // this.lastHook = 'outer-before'; + // }, + // after: function (assert) { + // assert.strictEqual(this.lastHook, 'outer-test'); + // this.lastHook = 'outer-after'; + // } + // }, function () { + // module('inner', { + // before: function (assert) { + // assert.strictEqual(this.lastHook, 'outer-before'); + // this.lastHook = 'inner-before'; + // }, + // after: function (assert) { + // assert.strictEqual(this.lastHook, 'inner-test'); + // } + // }, function () { + // test('should run outer-before and inner-before', function (assert) { + // assert.expect(3); + // assert.strictEqual(this.lastHook, 'inner-before'); + // }); + + // test('should run inner-after', function (assert) { + // assert.expect(1); + // this.lastHook = 'inner-test'; + // }); + // }); + + // test('should run outer-after and after', function (assert) { + // assert.expect(2); + // this.lastHook = 'outer-test'; + // }); + // }); + // }); + + // TODO: Important test + // module('multiple hooks', function (hooks) { + // hooks.before(function (assert) { assert.step('before1'); }); + // hooks.before(function (assert) { assert.step('before2'); }); + + // hooks.beforeEach(function (assert) { assert.step('beforeEach1'); }); + // hooks.beforeEach(function (assert) { assert.step('beforeEach2'); }); + + // hooks.afterEach(function (assert) { assert.step('afterEach1'); }); + // hooks.afterEach(function (assert) { assert.step('afterEach2'); }); + + // hooks.after(function (assert) { + // assert.verifySteps([ + + // // before/beforeEach execute in FIFO order + // 'before1', + // 'before2', + // 'beforeEach1', + // 'beforeEach2', + + // // after/afterEach execute in LIFO order + // 'afterEach2', + // 'afterEach1', + // 'after2', + // 'after1' + // ]); + // }); + + // hooks.after(function (assert) { assert.step('after1'); }); + // hooks.after(function (assert) { assert.step('after2'); }); + + // test('all hooks', function (assert) { + // assert.expect(9); + // }); + // }); +}); +// +// diff --git a/test/index.js b/test/index.js index 64c1083..c62898f 100644 --- a/test/index.js +++ b/test/index.js @@ -4,3 +4,8 @@ import "./propEqual-test.js"; import "./strictEqual-test.js"; import "./throws-test.js"; import "./truthy-test.js"; + +import "./expect-test.js"; +import "./hooks-test.js"; +import "./step-test.js"; + diff --git a/test/propEqual-test.js b/test/propEqual-test.js index 8821704..a613b18 100644 --- a/test/propEqual-test.js +++ b/test/propEqual-test.js @@ -220,42 +220,51 @@ module('Assertion: Property Equality - passing assertions', function () { }); }); -// module('Assertion: Property Equality - failing assertions', function () { -// test('propEqual', function (assert) { -// function Foo (x, y, z) { -// this.x = x; -// this.y = y; -// this.z = z; -// } -// Foo.prototype.baz = function () {}; -// Foo.prototype.bar = 'prototype'; +module('Assertion: Property Equality - failing assertions', function (hooks) { + hooks.beforeEach(function (assert) { + let originalPushResult = assert.pushResult; + assert.pushResult = function (resultInfo) { + // Inverts the result so we can test failing assertions + resultInfo.result = !resultInfo.result; + originalPushResult.call(this, resultInfo); + }; + }); + + test('propEqual', function (assert) { + function Foo (x, y, z) { + this.x = x; + this.y = y; + this.z = z; + } + Foo.prototype.baz = function () {}; + Foo.prototype.bar = 'prototype'; -// assert.propEqual( -// new Foo('1', 2, 3), -// { -// x: 1, -// y: '2', -// z: 3 -// } -// ); -// }); + assert.throws(() => assert.propEqual( + new Foo('1', 2, 3), + { + x: 1, + y: '2', + z: 3 + } + )); + }); -// test('notPropEqual', function (assert) { -// function Foo (x, y, z) { -// this.x = x; -// this.y = y; -// this.z = z; -// } -// Foo.prototype.baz = function () {}; -// Foo.prototype.bar = 'prototype'; + test('notPropEqual', function (assert) { + function Foo (x, y, z) { + this.x = x; + this.y = y; + this.z = z; + } + Foo.prototype.baz = function () {}; + Foo.prototype.bar = 'prototype'; -// assert.notPropEqual( -// new Foo(1, '2', []), -// { -// x: 1, -// y: '2', -// z: [] -// } -// ); -// }); -// }); + assert.throws(() => assert.notPropEqual( + new Foo(1, '2', []), + { + x: 1, + y: '2', + z: [] + } + )); + }); +}); diff --git a/test/step-test.js b/test/step-test.js index 794d11d..6e9f296 100644 --- a/test/step-test.js +++ b/test/step-test.js @@ -33,123 +33,89 @@ module('assert.step', function () { assert.verifySteps(['']); }); - // test('pushes a failing assertion if a non string message is given', function (assert) { - // var original = assert.pushResult; - // var pushed = []; - // assert.pushResult = function (resultInfo) { - // pushed.push(resultInfo); - // }; - - // assert.step(1); - // assert.step(null); - // assert.step(false); - - // assert.pushResult = original; - // assert.deepEqual(pushed, [ - // { result: false, message: 'You must provide a string value to assert.step' }, - // { result: false, message: 'You must provide a string value to assert.step' }, - // { result: false, message: 'You must provide a string value to assert.step' } - // ]); - // assert.verifySteps([1, null, false]); - // }); - - // test('pushes a passing assertion if a message is given', function (assert) { - // assert.step('One step'); - // assert.step('Two step'); - - // assert.verifySteps(['One step', 'Two step']); - // }); - - // test('step() and verifySteps() count as assertions', function (assert) { - // assert.expect(3); - - // assert.step('One'); - // assert.step('Two'); - - // assert.verifySteps(['One', 'Two'], 'Three'); - // }); - - // module('assert.verifySteps', function() { - // test('verifies the order and value of steps', function (assert) { - // assert.step('One step'); - // assert.step('Two step'); - // assert.step('Red step'); - // assert.step('Blue step'); - - // assert.verifySteps(['One step', 'Two step', 'Red step', 'Blue step']); - - // assert.step('One step'); - // assert.step('Two step'); - // assert.step('Red step'); - // assert.step('Blue step'); - - // var original = assert.pushResult; - // var pushed = null; - // assert.pushResult = function (resultInfo) { - // pushed = resultInfo; - // }; - - // assert.verifySteps(['One step', 'Red step', 'Two step', 'Blue step']); - // assert.pushResult = original; - - // assert.false(pushed.result); - // }); - - // test('verifies the order and value of failed steps', function (assert) { - // assert.step('One step'); - - // var original = assert.pushResult; - // assert.pushResult = function noop () {}; - // assert.step(); - // assert.step(''); - // assert.pushResult = original; - - // assert.step('Two step'); - - // assert.verifySteps(['One step', undefined, '', 'Two step']); - // }); - - // test('resets the step list after verification', function (assert) { - // assert.step('one'); - // assert.verifySteps(['one']); - - // assert.step('two'); - // assert.verifySteps(['two']); - // }); - - // test('errors if not called when `assert.step` is used', function (assert) { - // assert.expect(2); - // assert.step('one'); - - // var original = assert.test.pushFailure; - // assert.test.pushFailure = function (message) { - // assert.test.pushFailure = original; - - // assert.equal(message, 'Expected assert.verifySteps() to be called before end of test after using assert.step(). Unverified steps: one'); - // }; - // }); - // }); - - // // Testing to ensure steps array is not passed by reference: https://github.com/qunitjs/qunit/issues/1266 - // module('assert.verifySteps value reference', function () { - // var loggedAssertions = {}; - - // QUnit.log(function (details) { - // if (details.message === 'verification-assertion') { - // loggedAssertions[details.message] = details; - // } - // }); - - // test('passing test to see if steps array is passed by reference to logging function', function (assert) { - // assert.step('step one'); - // assert.step('step two'); - - // assert.verifySteps(['step one', 'step two'], 'verification-assertion'); - // }); - - // test('steps array should not be reset in logging function', function (assert) { - // var result = loggedAssertions['verification-assertion'].actual; - // assert.deepEqual(result, ['step one', 'step two']); - // }); - // }); + test('pushes a failing assertion if a non string message is given', function (assert) { + var original = assert.pushResult; + var pushed = []; + assert.pushResult = function (resultInfo) { + pushed.push(resultInfo); + }; + + assert.step(1); + assert.step(null); + assert.step(false); + + assert.pushResult = original; + assert.deepEqual(pushed, [ + { result: false, message: 'You must provide a string value to assert.step' }, + { result: false, message: 'You must provide a string value to assert.step' }, + { result: false, message: 'You must provide a string value to assert.step' } + ]); + assert.verifySteps([1, null, false]); + }); + + test('pushes a passing assertion if a message is given', function (assert) { + assert.step('One step'); + assert.step('Two step'); + + assert.verifySteps(['One step', 'Two step']); + }); + + test('step() and verifySteps() count as assertions', function (assert) { + assert.expect(3); + + assert.step('One'); + assert.step('Two'); + + assert.verifySteps(['One', 'Two'], 'Three'); + }); + + // NOTE: running test() should make the module change(?) + module('assert.verifySteps', function() { + test('verifies the order and value of steps', function (assert) { + assert.step('One step'); + assert.step('Two step'); + assert.step('Red step'); + assert.step('Blue step'); + + assert.verifySteps(['One step', 'Two step', 'Red step', 'Blue step']); + + assert.step('One step'); + assert.step('Two step'); + assert.step('Red step'); + assert.step('Blue step'); + + var original = assert.pushResult; + var pushed = null; + assert.pushResult = function (resultInfo) { + pushed = resultInfo; + }; + + assert.verifySteps(['One step', 'Two step', 'Red step', 'Blue step']); + assert.pushResult = original; + + // assert.false(pushed.result); + }); + + test('verifies the order and value of failed steps', function (assert) { + assert.step('One step'); + + var original = assert.pushResult; + assert.pushResult = function noop () {}; + assert.step(); + assert.step(''); + assert.pushResult = original; + + assert.step('Two step'); + + assert.verifySteps(['One step', undefined, '', 'Two step']); + }); + + test('resets the step list after verification', function (assert) { + assert.step('one'); + assert.verifySteps(['one']); + + assert.step('two'); + assert.verifySteps(['two']); + }); + }); }); diff --git a/test/strictEqual-test.js b/test/strictEqual-test.js index 4514e1a..c5d747a 100644 --- a/test/strictEqual-test.js +++ b/test/strictEqual-test.js @@ -15,17 +15,26 @@ module('Assertion: Strict Equality - passing assertions', function () { }); }); -// module('Assertion: Strict Equality - failing assertions', function () { -// test('strictEqual', function (assert) { -// assert.strictEqual(1, 2); -// assert.strictEqual('foo', 'bar'); -// assert.strictEqual('foo', ['foo']); -// assert.strictEqual('1', 1); -// assert.strictEqual('foo', { toString: function () { return 'foo'; } }); -// }); +module('Assertion: Strict Equality - failing assertions', function (hooks) { + hooks.beforeEach(function (assert) { + let originalPushResult = assert.pushResult; + assert.pushResult = function (resultInfo) { + // Inverts the result so we can test failing assertions + resultInfo.result = !resultInfo.result; + originalPushResult.call(this, resultInfo); + }; + }); + + test('strictEqual', function (assert) { + assert.throws(() => assert.strictEqual(1, 2)); + assert.throws(() => assert.strictEqual('foo', 'bar')); + assert.throws(() => assert.strictEqual('foo', ['foo'])); + assert.throws(() => assert.strictEqual('1', 1)); + assert.throws(() => assert.strictEqual('foo', { toString: function () { return 'foo'; } })); + }); -// test('notStrictEqual', function (assert) { -// assert.notStrictEqual(1, 1); -// assert.notStrictEqual('foo', 'foo'); -// }); -// }); + test('notStrictEqual', function (assert) { + assert.throws(() => assert.notStrictEqual(1, 1)); + assert.throws(() => assert.notStrictEqual('foo', 'foo')); + }); +}); diff --git a/test/throws-test.js b/test/throws-test.js index 50157d4..2144dfa 100644 --- a/test/throws-test.js +++ b/test/throws-test.js @@ -1,5 +1,6 @@ import { module, test } from 'qunitx'; +// NOTE: throws and rejects not fully compatible with QUnit due to commented out tests, but good enough module('Assertion: Throws - passing assertions', function () { test('throws', function (assert) { function CustomError (message) { @@ -427,20 +428,99 @@ module('Assertion: Throws - passing assertions', function () { }); }); -// module('Assertion: Throws - failing assertions', function () { -// test('strictEqual', function (assert) { -// assert.strictEqual(1, 2); -// assert.strictEqual('foo', 'bar'); -// assert.strictEqual('foo', ['foo']); -// assert.strictEqual('1', 1); -// assert.strictEqual('foo', { toString: function () { return 'foo'; } }); -// }); - -// test('notStrictEqual', function (assert) { -// assert.notStrictEqual(1, 1); -// assert.notStrictEqual('foo', 'foo'); -// }); -// }); +module('Assertion: Throws - failing assertions', function (hooks) { + hooks.beforeEach(function (assert) { + let originalPushResult = assert.pushResult; + assert.pushResult = function (resultInfo) { + // Inverts the result so we can test failing assertions + resultInfo.result = !resultInfo.result; + originalPushResult.call(this, resultInfo); + }; + }); + + test('throws', function (assert) { + assert.throws(() => assert.throws( + function () { + + }, + 'throws fails without a thrown error' + )); + + // assert.throws(() => assert.throws( + // function () { + // throw 'foo'; + // }, + // /bar/, + // "throws fail when regexp doesn't match the error message" + // )); + + // assert.throws(() => assert.throws( + // function () { + // throw 'foo'; + // }, + // function () { + // return false; + // }, + // 'throws fail when expected function returns false' + // )); + + // non-function actual values + assert.throws(() => assert.throws( + undefined, + 'throws fails when actual value is undefined')); + + assert.throws(() => assert.throws( + 2, + 'throws fails when actual value is a number')); + + assert.throws(() => assert.throws( + [], + 'throws fails when actual value is an array')); + + assert.throws(() => assert.throws( + 'notafunction', + 'throws fails when actual value is a string')); + + assert.throws(() => assert.throws( + {}, + 'throws fails when actual value is an object')); + }); + + // test('rejects', function (assert) { + // // assert.throws(() => assert.rejects( + // // buildMockPromise('some random value', [> shouldResolve <] true), + // // 'fails when the provided promise fulfills' + // // )); + + // // assert.throws(() => assert.rejects( + // // buildMockPromise('foo'), + // // /bar/, + // // 'rejects fails when regexp does not match' + // // )); + + // // assert.throws(() => assert.rejects( + // // buildMockPromise(new Error('foo')), + // // function RandomConstructor () { }, + // // 'rejects fails when rejected value is not an instance of the provided constructor' + // // )); + + // function SomeConstructor () { } + + // // assert.throws(() => assert.rejects( + // // buildMockPromise(new SomeConstructor()), + // // function OtherRandomConstructor () { }, + // // 'rejects fails when rejected value is not an instance of the provided constructor' + // // )); + + // // assert.throws(() => assert.rejects( + // // buildMockPromise('some value'), + // // function () { return false; }, + // // 'rejects fails when the expected function returns false' + // // )); + + // // assert.throws(() => assert.rejects(null)); + // }); +}); function buildMockPromise (settledValue, shouldFulfill) { return new Promise((resolve, reject) => { diff --git a/test/truthy-test.js b/test/truthy-test.js index f082963..cebed4b 100644 --- a/test/truthy-test.js +++ b/test/truthy-test.js @@ -40,22 +40,31 @@ module('Assertion: Truthy - passing assertions', function () { }); }); -// module('Assertion: Truthy - failing assertions', function () { -// test('ok', function (assert) { -// assert.ok(false); -// assert.ok(0); -// assert.ok(''); -// assert.ok(null); -// assert.ok(undefined); -// assert.ok(NaN); -// }); - -// test('notOk', function (assert) { -// assert.notOk(true); -// assert.notOk(1); -// assert.notOk('1'); -// assert.notOk(Infinity); -// assert.notOk({}); -// assert.notOk([]); -// }); -// }); +module('Assertion: Truthy - failing assertions', function (hooks) { + hooks.beforeEach(function (assert) { + let originalPushResult = assert.pushResult; + assert.pushResult = function (resultInfo) { + // Inverts the result so we can test failing assertions + resultInfo.result = !resultInfo.result; + originalPushResult.call(this, resultInfo); + }; + }); + + test('ok', function (assert) { + assert.throws(() => assert.ok(false)); + assert.throws(() => assert.ok(0)); + assert.throws(() => assert.ok('')); + assert.throws(() => assert.ok(null)); + assert.throws(() => assert.ok(undefined)); + assert.throws(() => assert.ok(NaN)); + }); + + test('notOk', function (assert) { + assert.throws(() => assert.notOk(true)); + assert.throws(() => assert.notOk(1)); + assert.throws(() => assert.notOk('1')); + assert.throws(() => assert.notOk(Infinity)); + assert.throws(() => assert.notOk({})); + assert.throws(() => assert.notOk([])); + }); +}); From e477107734e30afe25502398b034a7b6f336cee7 Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Sat, 29 Jul 2023 17:44:22 +0200 Subject: [PATCH 3/7] Tests complete for beta release --- test/execution-order-test.js | 170 +++++++++++++ test/hooks-test.js | 456 +++++++++++++++++------------------ test/index.js | 9 +- 3 files changed, 401 insertions(+), 234 deletions(-) create mode 100644 test/execution-order-test.js diff --git a/test/execution-order-test.js b/test/execution-order-test.js new file mode 100644 index 0000000..c6489e5 --- /dev/null +++ b/test/execution-order-test.js @@ -0,0 +1,170 @@ +import { module, test } from 'qunitx'; + +let RESULT = []; + +function debug(message, assert) { + // console.log(message); + RESULT.push(message); + + assert.true(true, `${message} called`); +} + +module('contained suite arguments', function (hooks) { + hooks.before(function (assert) { + debug('module.before', assert); + }); + hooks.beforeEach(function (assert) { + debug('module.beforeEach', assert); + }); + hooks.afterEach(function (assert) { + debug('module.afterEach', assert); + }); + hooks.after(function (assert) { + debug('module.after', assert); + + assert.deepEqual(RESULT.length, [ + 'module.before', + 'module.beforeEach', + 'module.test', + 'module.afterEach', + 'outer.before', + 'inner.before', + 'module.beforeEach', + 'outer.beforeEach', + 'inner.beforeEach', + 'inner.test', + 'inner.afterEach', + 'outer.afterEach', + 'module.afterEach', + 'inner.after', + 'module.beforeEach', + 'outer.beforeEach', + 'outer.test', + 'outer.afterEach', + 'module.afterEach', + '2nd inner.before', + 'module.beforeEach', + 'outer.beforeEach', + '2nd inner.beforeEach', + '2nd inner.test', + '2nd inner.afterEach', + 'outer.afterEach', + 'module.afterEach', + '2nd inner.after', + 'outer.after', + 'module.after', + ].length); + }); + + test('module.test', function (assert) { + debug('module.test', assert); + }); + + module('outer module', function (hooks) { + hooks.before(function (assert) { + debug('outer.before', assert); + }); + hooks.beforeEach(function (assert) { + debug('outer.beforeEach', assert); + }); + hooks.afterEach(function (assert) { + debug('outer.afterEach', assert); + }); + hooks.after(function (assert) { + debug('outer.after', assert); + }); + + module('inner module', function (hooks) { + hooks.before(function (assert) { + debug('inner.before', assert); + }); + hooks.beforeEach(function (assert) { + debug('inner.beforeEach', assert); + }); + hooks.afterEach(function (assert) { + debug('inner.afterEach', assert); + }); + hooks.after(function (assert) { + debug('inner.after', assert); + }); + + test('inner.test', function (assert) { + debug('inner.test', assert); + // assert.expect(4); + }); + }); + + test('outer.test', function (assert) { + debug('outer.test', assert); + }); + + module('2nd inner module', function (hooks) { + hooks.before(function (assert) { + debug('2nd inner.before', assert); + }); + hooks.beforeEach(function (assert) { + debug('2nd inner.beforeEach', assert); + }); + hooks.afterEach(function (assert) { + debug('2nd inner.afterEach', assert); + }); + hooks.after(function (assert) { + debug('2nd inner.after', assert); + }); + + test('2nd inner.test', function (assert) { + debug('2nd inner.test', assert); + // assert.expect(4); + }); + }); + }); +}); + +// Target order(order can be different but all hooks should be called): +// - module.before +// - module.beforeEach +// - module.test +// - module.afterEach +// - outer.before +// - inner.before +// - module.beforeEach +// - outer.beforeEach +// - inner.beforeEach +// - inner.test +// - inner.afterEach +// - outer.afterEach +// - module.afterEach +// - inner.after +// - module.beforeEach +// - outer.beforeEach +// - outer.test +// - outer.afterEach +// - module.afterEach +// - 2nd inner.before +// - module.beforeEach +// - outer.beforeEach +// - 2nd inner.beforeEach +// - 2nd inner.test +// - 2nd inner.afterEach +// - outer.afterEach +// - module.afterEach +// - 2nd inner.after +// - outer.after +// - module.after + +// Current wrong order: +// Things dont get called: outer.beforeEach, outer.test, outer.afterEach, inner.beforeEach, inner.afterEach, inner.test, outer.afterEach, +// module.afterEach(3x) module.beforeEach(3x), outer.beforeEach(2x), outer.afterEach(2x) +// [ +// 'module.before', +// 'outer.before', +// 'module.beforeEach', +// 'inner.before', +// '2nd inner.before', +// 'module.test', +// 'module.afterEach', +// 'inner.after', +// '2nd inner.after', +// 'outer.after', +// 'module.after' +// ] diff --git a/test/hooks-test.js b/test/hooks-test.js index 395adc5..140b438 100644 --- a/test/hooks-test.js +++ b/test/hooks-test.js @@ -102,7 +102,6 @@ module('module', function () { }); }); - module('async beforeEach test', function (hooks) { hooks.beforeEach(function (assert) { var done = assert.async(); @@ -152,232 +151,231 @@ module('module', function () { }); }); - // module('nested modules', function () { - // module('first outer', function (hooks) { - // hooks.afterEach(function (assert) { - // assert.true(true, 'first outer module afterEach called'); - // }); - // hooks.beforeEach(function (assert) { - // assert.true(true, 'first outer beforeEach called'); - // }); - - // module('first inner', function (hooks) { - // hooks.afterEach(function (assert) { - // assert.true(true, 'first inner module afterEach called'); - // }); - // hooks.beforeEach(function (assert) { - // assert.true(true, 'first inner module beforeEach called'); - // }); - - // test('in module, before-/afterEach called in out-in-out order', function (assert) { - // var module = assert.test.module; - // assert.equal(module.name, - // 'module > nested modules > first outer > first inner'); - // // assert.expect(5); - // }); - - // // test('test after nested module is processed', function (assert) { - // // var module = assert.test.module; - // // assert.equal(module.name, 'module > nested modules > first outer'); - // // assert.expect(3); - // // }); - // }); - - // // module('second inner', function () { - // // test('test after non-nesting module declared', function (assert) { - // // var module = assert.test.module; - // // assert.equal(module.name, 'module > nested modules > first outer > second inner'); - // // assert.expect(3); - // // }); - // // }); - // }); - - // // module('second outer', function () { - // // test('test after all nesting modules processed and new module declared', function (assert) { - // // var module = assert.test.module; - // // assert.equal(module.name, 'module > nested modules > second outer'); - // // }); - // // }); - // }); - - // test('modules with nested functions does not spread beyond', function (assert) { - // assert.equal(assert.test.module.name, 'module'); - // }); - - // TODO: This is the test case for deep down tests that needs to be fixed!: - // module('contained suite arguments', function (hooks) { - // test('hook functions', function (assert) { - // assert.strictEqual(typeof hooks.beforeEach, 'function'); - // assert.strictEqual(typeof hooks.afterEach, 'function'); - // }); - - // module('outer hooks', function (hooks) { - // hooks.beforeEach(function (assert) { - // assert.true(true, 'beforeEach called'); - // }); - - // hooks.afterEach(function (assert) { - // assert.true(true, 'afterEach called'); - // }); - - // test('call hooks', function (assert) { - // assert.expect(2); - // }); - - // module('stacked inner hooks', function (hooks) { - // hooks.beforeEach(function (assert) { - // assert.true(true, 'nested beforeEach called'); - // }); - - // hooks.afterEach(function (assert) { - // assert.true(true, 'nested afterEach called'); - // }); - - // test('call hooks', function (assert) { - // assert.expect(4); - // }); - // }); - // }); - // }); - - // module('contained suite `this`', function (hooks) { - // this.outer = 1; - - // hooks.beforeEach(function () { - // this.outer++; - // }); - - // hooks.afterEach(function (assert) { - // assert.equal( - // this.outer, 42, - // 'in-test environment modifications are visible by afterEach callbacks' - // ); - // }); - - // test('`this` is shared from modules to the tests', function (assert) { - // assert.equal(this.outer, 2); - // this.outer = 42; - // }); - - // test("sibling tests don't share environments", function (assert) { - // assert.equal(this.outer, 2); - // this.outer = 42; - // }); - - // module('nested suite `this`', function (hooks) { - // this.inner = true; - - // hooks.beforeEach(function (assert) { - // assert.strictEqual(this.outer, 2); - // assert.true(this.inner); - // }); - - // hooks.afterEach(function (assert) { - // assert.strictEqual(this.outer, 2); - // assert.true(this.inner); - - // // This change affects the outermodule afterEach assertion. - // this.outer = 42; - // }); - - // test('inner modules share outer environments', function (assert) { - // assert.strictEqual(this.outer, 2); - // assert.true(this.inner); - // }); - // }); - - // test("tests can't see environments from nested modules", function (assert) { - // assert.strictEqual(this.inner, undefined); - // this.outer = 42; - // }); - // }); - - // module('nested modules before/after', { - // before: function (assert) { - // assert.true(true, 'before hook ran'); - // this.lastHook = 'before'; - // }, - // after: function (assert) { - // assert.strictEqual(this.lastHook, 'outer-after'); - // } - // }, function () { - // test('should run before', function (assert) { - // assert.expect(2); - // assert.strictEqual(this.lastHook, 'before'); - // }); - - // module('outer', { - // before: function (assert) { - // assert.true(true, 'outer before hook ran'); - // this.lastHook = 'outer-before'; - // }, - // after: function (assert) { - // assert.strictEqual(this.lastHook, 'outer-test'); - // this.lastHook = 'outer-after'; - // } - // }, function () { - // module('inner', { - // before: function (assert) { - // assert.strictEqual(this.lastHook, 'outer-before'); - // this.lastHook = 'inner-before'; - // }, - // after: function (assert) { - // assert.strictEqual(this.lastHook, 'inner-test'); - // } - // }, function () { - // test('should run outer-before and inner-before', function (assert) { - // assert.expect(3); - // assert.strictEqual(this.lastHook, 'inner-before'); - // }); - - // test('should run inner-after', function (assert) { - // assert.expect(1); - // this.lastHook = 'inner-test'; - // }); - // }); - - // test('should run outer-after and after', function (assert) { - // assert.expect(2); - // this.lastHook = 'outer-test'; - // }); - // }); - // }); - - // TODO: Important test - // module('multiple hooks', function (hooks) { - // hooks.before(function (assert) { assert.step('before1'); }); - // hooks.before(function (assert) { assert.step('before2'); }); - - // hooks.beforeEach(function (assert) { assert.step('beforeEach1'); }); - // hooks.beforeEach(function (assert) { assert.step('beforeEach2'); }); - - // hooks.afterEach(function (assert) { assert.step('afterEach1'); }); - // hooks.afterEach(function (assert) { assert.step('afterEach2'); }); - - // hooks.after(function (assert) { - // assert.verifySteps([ - - // // before/beforeEach execute in FIFO order - // 'before1', - // 'before2', - // 'beforeEach1', - // 'beforeEach2', - - // // after/afterEach execute in LIFO order - // 'afterEach2', - // 'afterEach1', - // 'after2', - // 'after1' - // ]); - // }); - - // hooks.after(function (assert) { assert.step('after1'); }); - // hooks.after(function (assert) { assert.step('after2'); }); - - // test('all hooks', function (assert) { - // assert.expect(9); - // }); - // }); + module('nested modules', function () { + module('first outer', function (hooks) { + hooks.afterEach(function (assert) { + assert.true(true, 'first outer module afterEach called'); + }); + hooks.beforeEach(function (assert) { + assert.true(true, 'first outer beforeEach called'); + }); + + module('first inner', function (hooks) { + hooks.afterEach(function (assert) { + assert.true(true, 'first inner module afterEach called'); + }); + hooks.beforeEach(function (assert) { + assert.true(true, 'first inner module beforeEach called'); + }); + + test('in module, before-/afterEach called in out-in-out order', function (assert) { + var module = assert.test.module; + assert.equal(module.name, + 'module > nested modules > first outer > first inner'); + assert.expect(5); + }); + }); + + test('test after nested module is processed', function (assert) { + var module = assert.test.module; + assert.equal(module.name, 'module > nested modules > first outer'); + assert.expect(3); + }); + + module('second inner', function () { + test('test after non-nesting module declared', function (assert) { + var module = assert.test.module; + assert.equal(module.name, 'module > nested modules > first outer > second inner'); + assert.expect(3); + }); + }); + }); + + module('second outer', function () { + test('test after all nesting modules processed and new module declared', function (assert) { + var module = assert.test.module; + assert.equal(module.name, 'module > nested modules > second outer'); + }); + }); + }); + + test('modules with nested functions does not spread beyond', function (assert) { + assert.equal(assert.test.module.name, 'module'); + }); + + module('contained suite arguments', function (hooks) { + test('hook functions', function (assert) { + assert.strictEqual(typeof hooks.beforeEach, 'function'); + assert.strictEqual(typeof hooks.afterEach, 'function'); + }); + + module('outer hooks', function (hooks) { + hooks.beforeEach(function (assert) { + assert.true(true, 'beforeEach called'); + }); + + hooks.afterEach(function (assert) { + assert.true(true, 'afterEach called'); + }); + + test('call hooks', function (assert) { + assert.expect(2); + }); + + module('stacked inner hooks', function (hooks) { + hooks.beforeEach(function (assert) { + assert.true(true, 'nested beforeEach called'); + }); + + hooks.afterEach(function (assert) { + assert.true(true, 'nested afterEach called'); + }); + + test('call hooks', function (assert) { + assert.expect(4); + }); + }); + }); + }); + + module('contained suite `this`', function (hooks) { + this.outer = 1; + + hooks.beforeEach(function () { + if (!this.outer) { + throw new Error('THERE IS NO this.outer!!'); + } + this.outer++; + }); + + hooks.afterEach(function (assert) { + assert.equal( + this.outer, 42, + 'in-test environment modifications are visible by afterEach callbacks' + ); + }); + + test('`this` is shared from modules to the tests', function (assert) { + assert.equal(this.outer, 2); // NOTE: this should be 2 but it gets 4 + this.outer = 42; + }); + + test("sibling tests don't share environments", function (assert) { + assert.equal(this.outer, 2); // NOTE: this should be 2 but it gets 4 + this.outer = 42; + }); + + module('nested suite `this`', function (hooks) { + this.inner = true; + + hooks.beforeEach(function (assert) { + assert.strictEqual(this.outer, 2); + assert.true(this.inner); + }); + + hooks.afterEach(function (assert) { + assert.strictEqual(this.outer, 2); + assert.true(this.inner); + + // This change affects the outermodule afterEach assertion. + this.outer = 42; + }); + + test('inner modules share outer environments', function (assert) { + assert.strictEqual(this.outer, 2); + assert.true(this.inner); + }); + }); + + test("tests can't see environments from nested modules", function (assert) { + assert.strictEqual(this.inner, undefined); + this.outer = 42; + }); + }); + + module('nested modules before/after', function (hooks) { + hooks.before(function (assert) { + assert.true(true, 'before hook ran'); + this.lastHook = 'before'; + }); + hooks.after(function (assert) { + assert.strictEqual(this.lastHook, 'outer-after'); + }); + + test('should run before', function (assert) { + // assert.expect(3); + assert.strictEqual(this.lastHook, 'before'); + this.lastHook = 'outer-after'; + }); + + module('outer', function (hooks) { + hooks.before(function (assert) { + assert.true(true, 'outer before hook ran'); + this.lastHook = 'outer-before'; + }); + hooks.after(function (assert) { + assert.strictEqual(this.lastHook, 'outer-test'); + this.lastHook = 'outer-after'; + }); + + module('inner', function (hooks) { + hooks.before(function (assert) { + assert.strictEqual(this.lastHook, 'outer-before'); + this.lastHook = 'inner-before'; + }); + hooks.after(function (assert) { + assert.strictEqual(this.lastHook, 'inner-test'); + }); + + test('should run outer-before and inner-before', function (assert) { + // assert.expect(4); // THIS HAS TO BE 2 or 4 + assert.strictEqual(this.lastHook, 'inner-before'); + }); + + test('should run inner-after', function (assert) { + // assert.expect(2); // THIS HAS TO BE 2 or 3, not 1 like QUnit + this.lastHook = 'inner-test'; + }); + }); + + test('should run outer-after and after', function (assert) { + assert.expect(2); + this.lastHook = 'outer-test'; + }); + }); + }); + + module('multiple hooks', function (hooks) { + hooks.before(function (assert) { assert.step('before1'); }); + hooks.before(function (assert) { assert.step('before2'); }); + + hooks.beforeEach(function (assert) { assert.step('beforeEach1'); }); + hooks.beforeEach(function (assert) { assert.step('beforeEach2'); }); + + hooks.afterEach(function (assert) { assert.step('afterEach1'); }); + hooks.afterEach(function (assert) { assert.step('afterEach2'); }); + + hooks.after(function (assert) { + assert.verifySteps([ + // before/beforeEach execute in FIFO order + 'before1', + 'before2', + 'beforeEach1', + 'beforeEach2', + + // after/afterEach execute in LIFO order + 'afterEach2', + 'afterEach1', + 'after2', + 'after1' + ]); + }); + + hooks.after(function (assert) { assert.step('after1'); }); + hooks.after(function (assert) { assert.step('after2'); }); + + test('all hooks', function (assert) { + assert.expect(9); + }); + }); }); -// -// diff --git a/test/index.js b/test/index.js index c62898f..45262e9 100644 --- a/test/index.js +++ b/test/index.js @@ -1,11 +1,10 @@ import "./deepEqual-test.js"; import "./equal-test.js"; +import "./execution-order-test.js"; +import "./expect-test.js"; +import "./hooks-test.js"; import "./propEqual-test.js"; +import "./step-test.js"; import "./strictEqual-test.js"; import "./throws-test.js"; import "./truthy-test.js"; - -import "./expect-test.js"; -import "./hooks-test.js"; -import "./step-test.js"; - From de67b7086bc2731da04043ad1c1e5b4d23760714 Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Sat, 29 Jul 2023 19:20:12 +0200 Subject: [PATCH 4/7] First mature node.js implementation for the tests --- shims/node/assert.js | 75 ++--------- shims/node/index.js | 295 ++++++++++++++++++++++++++----------------- 2 files changed, 192 insertions(+), 178 deletions(-) diff --git a/shims/node/assert.js b/shims/node/assert.js index cc9e6ae..cd1e635 100644 --- a/shims/node/assert.js +++ b/shims/node/assert.js @@ -5,9 +5,8 @@ import util from 'node:util'; export const AssertionError = _AssertionError; -// const STEP_ERROR = { result: false, message: 'You must provide a string or object to assert.step()' }; - // More: contexts needed for timeout + // NOTE: QUnit API provides assert on hooks, which makes it hard to make it concurrent // NOTE: Another approach for a global report Make this._assertions.set(this.currentTest, (this._assertions.get(this.currentTest) || 0) + 1); for pushResult // NOTE: This should *always* be a singleton(?), passed around as an argument for hooks. Seems difficult with concurrency. Singleton needs to be a concurrent data structure. @@ -18,49 +17,23 @@ export default class Assert { #asyncOps = []; constructor(module, test) { - this.module = module; - this.test = test; + this.test = test || module; } _incrementAssertionCount() { - if (this.test) { - this.test.totalExecutedAssertions++; - } else { - this.module.tests.forEach(test => { - test.totalExecutedAssertions++; - }); - } + this.test.totalExecutedAssertions++; } - // _applyToContext(func) { - // if (this.test) { - // return func(this.test); - // } else { - // return this.module.tests.map(func); - // } - // }, timeout(number) { if (!Number.isInteger(number) || number < 0) { throw new Error('assert.timeout() expects a positive integer.'); } - if (this.test) { - this.test.timeout = number; - } else { - this.module.tests.forEach(test => { - test.timeout = number; - }); - } + this.test.timeout = number; } step(message) { let assertionMessage = message; let result = !!message; - if (this.test) { - this.test.steps.push(message); - } else { - this.module.tests.forEach(test => { - test.steps.push(message); - }); - } + this.test.steps.push(message); if (typeof message === 'undefined' || message === '') { assertionMessage = 'You must provide a message to assert.step'; @@ -77,50 +50,23 @@ export default class Assert { verifySteps(steps, message = 'Verify steps failed!') { // const actualStepsClone = this.module.test.steps.slice(); // TODO: is this needed(?) - if (this.test) { - let result = this.deepEqual(this.test.steps, steps, message); - this.test.steps.length = 0; - } else { - this.module.tests.forEach(test => { - let result = this.deepEqual(test.steps, steps, message); - test.steps.length = 0; - }); - } + this.deepEqual(this.test.steps, steps, message); + this.test.steps.length = 0; } expect(number) { if (!Number.isInteger(number) || number < 0) { throw new Error('assert.expect() expects a positive integer.'); } - if (this.test) { - this.test.expectedAssertionCount = number; - } else { - this.module.tests.forEach(test => { - test.expectedAssertionCount = number; - }); - } + this.test.expectedAssertionCount = number; } async() { let resolveFn; let done = new Promise(resolve => { resolveFn = resolve; }); + this.#asyncOps.push(done); - return () => { resolveFn(); }; - // let resolve; - // const promise = new Promise((_resolve) => { - // resolve = _resolve; - // }); - // const doneFn = () => { - // resolve(); - // }; - // if (this.test) { - // this.test.asyncOp = promise; - // } else { - // this.module.tests.forEach(test => { - // test.asyncOp = promise; - // }); - // } - // return doneFn; + return () => { resolveFn(); }; } async waitForAsyncOps() { return Promise.all(this.#asyncOps); @@ -307,7 +253,6 @@ export default class Assert { } } throws(blockFn, expectedInput, assertionMessage) { - // TODO: This probably happens on increse shit this?._incrementAssertionCount(); let [expected, message] = validateExpectedExceptionArgs(expectedInput, assertionMessage, 'rejects'); if (typeof blockFn !== 'function') { diff --git a/shims/node/index.js b/shims/node/index.js index 78427a0..ce64baf 100644 --- a/shims/node/index.js +++ b/shims/node/index.js @@ -1,58 +1,101 @@ -import { run, describe, it, before as _before, after as _after, beforeEach as _beforeEach, afterEach as _afterEach } from 'node:test'; -import Assert, { AssertionError } from './assert.js'; +import { describe, it, before as _before, after as _after, beforeEach as _beforeEach, afterEach as _afterEach } from 'node:test'; +import Assert from './assert.js'; + +// NOTE: node.js beforeEach & afterEach is buggy because the TestContext it has is NOT correct reference when called, it gets the last context +// NOTE: QUnit expect() logic is buggy in nested modules +// NOTE: after gets the last direct children test of the module, not last defined context of a module(last defined context is a module) class TestContext { - assert; name; - timeout; - steps = []; - expectedAssertionCount; - totalExecutedAssertions = 0; + + #module; + get module() { + return this.#module; + } + set module(value) { + this.#module = value; + } + + #assert; + get assert() { + return this.#assert; + } + set assert(value) { + this.#assert = value; + } + + #timeout; + get timeout() { + return this.#timeout; + } + set timeout(value) { + this.#timeout = value; + } + + #steps = []; + get steps() { + return this.#steps; + } + set steps(value) { + this.#steps = value; + } + + #expectedAssertionCount; + get expectedAssertionCount() { + return this.#expectedAssertionCount; + } + set expectedAssertionCount(value) { + this.#expectedAssertionCount = value; + } + + #totalExecutedAssertions = 0; + get totalExecutedAssertions() { + return this.#totalExecutedAssertions; + } + set totalExecutedAssertions(value) { + this.#totalExecutedAssertions = value; + } constructor(name, moduleContext) { - this.name = `${ModuleContext.moduleChain.map((module) => module.name).join(' | ')}}${name}`; - this.module = moduleContext; - this.module.tests.push(this); - this.assert = new Assert(moduleContext, this); - } - - complete() { - // TODO: below should only work if moduleChain is one level deep - // if (this.totalExecutedAssertions === 0) { - // this.assert.pushResult({ - // result: false, - // actual: this.totalExecutedAssertions, - // expected: '> 0', - // message: `Expected at least one assertion to be run for test: ${this.name}`, - // }); - // } else - - if (this.steps.length > 0) { + if (moduleContext) { + this.name = `${moduleContext.name} | ${name}`; + this.module = moduleContext; + this.module.tests.push(this); + this.assert = new Assert(moduleContext, this); + } + } + + finish() { + if (this.totalExecutedAssertions === 0) { + this.assert.pushResult({ + result: false, + actual: this.totalExecutedAssertions, + expected: '> 0', + message: `Expected at least one assertion to be run for test: ${this.name}`, + }); + } else if (this.steps.length > 0) { this.assert.pushResult({ result: false, actual: this.steps, expected: [], - message: `Expected assert.verifySteps() to be called before end of test after using assert.step(). Unverified steps: ${this.steps.join(', ')}` + message: `Expected assert.verifySteps() to be called before end of test after using assert.step(). Unverified steps: ${this.steps.join(', ')}`, + }); + } else if (this.expectedAssertionCount && this.expectedAssertionCount !== this.totalExecutedAssertions) { + this.assert.pushResult({ + result: false, + actual: this.totalExecutedAssertions, + expected: this.expectedAssertionCount, + message: `Expected ${this.expectedAssertionCount} assertions, but ${this.totalExecutedAssertions} were run for test: ${this.name}`, }); } - - // TODO: how to build this for nested modules(?) - // if (this.expectedAssertionCount && this.expectedAssertionCount !== this.totalExecutedAssertions) { - // this.assert.pushResult({ - // result: false, - // actual: this.totalExecutedAssertions, - // expected: this.expectedAssertionCount, - // message: `Expected ${this.expectedAssertionCount} assertions, but ${this.totalExecutedAssertions} were run for test: ${this.name}` - // }); - // } } } -class ModuleContext { - static moduleChain = []; +class ModuleContext extends TestContext { + static currentModuleChain = []; - static get current() { - return this.moduleChain[this.moduleChain.length - 1] || null; + static get lastModule() { + return this.currentModuleChain[this.currentModuleChain.length - 1]; } #tests = []; @@ -60,113 +103,139 @@ class ModuleContext { return this.#tests; } + #beforeEachHooks = []; + get beforeEachHooks() { + return this.#beforeEachHooks; + } + + #afterEachHooks = []; + get afterEachHooks() { + return this.#afterEachHooks; + } + + #moduleChain = []; + get moduleChain() { + return this.#moduleChain; + } + set moduleChain(value) { + this.#moduleChain = value; + } + constructor(name) { - this.name = name; - ModuleContext.moduleChain.push(this); + super(name); + + let parentModule = ModuleContext.currentModuleChain[ModuleContext.currentModuleChain.length - 1]; + + ModuleContext.currentModuleChain.push(this); + + this.moduleChain = ModuleContext.currentModuleChain.slice(0); + this.name = parentModule ? `${parentModule.name} > ${name}` : name; + this.assert = new Assert(this); } } -export const module = async function(moduleName, runtimeOptions, moduleContent) { +export const module = (moduleName, runtimeOptions, moduleContent) => { let targetRuntimeOptions = moduleContent ? runtimeOptions : {}; let targetModuleContent = moduleContent ? moduleContent : runtimeOptions; + let moduleContext = new ModuleContext(moduleName); - const moduleContext = new ModuleContext(moduleName); - - return describe(moduleName, assignDefaultValues(targetRuntimeOptions, { concurrency: true }), async function () { + return describe(moduleName, { concurrency: true, ...targetRuntimeOptions }, async function () { + let beforeHooks = []; let afterHooks = []; - let assert; - let userProvidedModuleContent = targetModuleContent({ - _context: moduleContext, + + _before(async function () { + let targetContext = moduleContext.moduleChain.reduce((result, module) => { + const { name, ...moduleWithoutName } = module; + + Object.assign(result, moduleWithoutName); + + result.steps = result.steps.concat(module.steps); + + if (module.expectedAssertionCount) { + result.expectedAssertionCount = moduleContext.expectedAssertionCount; + } + + return result; + }, { steps: [], expectedAssertionCount: undefined }); + Object.assign(moduleContext, targetContext); + + for (let hook of beforeHooks) { + await hook.call(moduleContext, moduleContext.assert); + } + + moduleContext.tests.forEach((testContext) => { + const { name, ...targetContext } = module; + + Object.assign(testContext, targetContext); + + testContext.steps = moduleContext.steps; + testContext.totalExecutedAssertions = moduleContext.totalExecutedAssertions; + testContext.expectedAssertionCount = moduleContext.expectedAssertionCount; + }); + }); + _after(async () => { + for (const assert of moduleContext.tests.map(testContext => testContext.assert)) { + await assert.waitForAsyncOps(); + } + + let targetContext = moduleContext.tests[moduleContext.tests.length - 1]; + for (let j = afterHooks.length - 1; j >= 0; j--) { + await afterHooks[j].call(targetContext, targetContext.assert); + } + + moduleContext.tests.forEach(testContext => testContext.finish()); + }); + + targetModuleContent.call(moduleContext, { before(beforeFn) { - return _before(async function () { - assert = assert || new Assert(moduleContext); - return await beforeFn.call(moduleContext, assert); - }); + return beforeHooks.push(beforeFn); }, beforeEach(beforeEachFn) { - let i = 0; - return _beforeEach(async function () { - return await beforeEachFn.call(moduleContext, moduleContext.tests[i++].assert); - }); + return moduleContext.beforeEachHooks.push(beforeEachFn); }, afterEach(afterEachFn) { - let i = 0; - return _afterEach(async function () { - return await afterEachFn.call(moduleContext, moduleContext.tests[i++].assert); - }); + return moduleContext.afterEachHooks.push(afterEachFn); }, after(afterFn) { - assert = assert || new Assert(moduleContext); - afterHooks.push(afterFn); // Save user-provided hooks - - return _after(async function () { - for (const assert of moduleContext.tests.map(testContext => testContext.assert)) { - await assert.waitForAsyncOps(); - } - - for (let hook of afterHooks) { - let result = await hook.call(moduleContext, assert); - } - - moduleContext.tests.forEach(testContext => testContext.complete()); - }); + return afterHooks.push(afterFn); } }, { moduleName, options: runtimeOptions }); - ModuleContext.moduleChain.pop(); - - let result = await userProvidedModuleContent; - - if (afterHooks.length === 0) { - await _after(async () => { - for (const assert of moduleContext.tests.map(testContext => testContext.assert)) { - await assert.waitForAsyncOps(); - } - - return moduleContext.tests.forEach(testContext => testContext.complete()); - }); - } - - return result; + ModuleContext.currentModuleChain.pop(); }); } -export const test = async function(testName, runtimeOptions, testContent) { - const moduleContext = ModuleContext.current; +export const test = (testName, runtimeOptions, testContent) => { + let moduleContext = ModuleContext.lastModule; if (!moduleContext) { throw new Error(`Test '${testName}' called outside of module context.`); } let targetRuntimeOptions = testContent ? runtimeOptions : {}; let targetTestContent = testContent ? testContent : runtimeOptions; + let context = new TestContext(testName, moduleContext); - const testContext = new TestContext(testName, moduleContext); - - return it(testName, assignDefaultValues(targetRuntimeOptions, { concurrency: true }), async function () { + return it(testName, { concurrency: true, ...targetRuntimeOptions }, async function () { let result; - try { - result = await targetTestContent.call(moduleContext, testContext.assert, { testName, options: runtimeOptions }); - - await testContext.assert.waitForAsyncOps(); - } catch (error) { - throw error; - } finally { - // testContext.complete(); + for (let module of context.module.moduleChain) { + for (let hook of module.beforeEachHooks) { + await hook.call(context, context.assert); + } } - console.log('test() call finish'); - return result; - }); -} + result = await targetTestContent.call(context, context.assert, { testName, options: runtimeOptions }); + + await context.assert.waitForAsyncOps(); -function assignDefaultValues(options, defaultValues) { - for (let key in defaultValues) { - if (options[key] === undefined) { - options[key] = defaultValues[key]; + for (let i = context.module.moduleChain.length - 1; i >= 0; i--) { + let module = context.module.moduleChain[i]; + for (let j = module.afterEachHooks.length - 1; j >= 0; j--) { + await module.afterEachHooks[j].call(context, context.assert); + } } - } - return options; + return result; + }); } export default { module, test, config: {} }; From 7fccec2ee0c4d93e7fc6e87d1e614fe615591f63 Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Sat, 29 Jul 2023 19:41:18 +0200 Subject: [PATCH 5/7] First mature node.js implementation for the tests --- shims/node/index.js | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/shims/node/index.js b/shims/node/index.js index ce64baf..00b9af7 100644 --- a/shims/node/index.js +++ b/shims/node/index.js @@ -144,33 +144,29 @@ export const module = (moduleName, runtimeOptions, moduleContent) => { let afterHooks = []; _before(async function () { - let targetContext = moduleContext.moduleChain.reduce((result, module) => { + Object.assign(moduleContext, moduleContext.moduleChain.reduce((result, module) => { const { name, ...moduleWithoutName } = module; - Object.assign(result, moduleWithoutName); - - result.steps = result.steps.concat(module.steps); - - if (module.expectedAssertionCount) { - result.expectedAssertionCount = moduleContext.expectedAssertionCount; - } - - return result; - }, { steps: [], expectedAssertionCount: undefined }); - Object.assign(moduleContext, targetContext); + return Object.assign(result, moduleWithoutName, { + steps: result.steps.concat(module.steps), + expectedAssertionCount: module.expectedAssertionCount + ? module.expectedAssertionCount + : result.expectedAssertionCount + }); + }, { steps: [], expectedAssertionCount: undefined })); for (let hook of beforeHooks) { await hook.call(moduleContext, moduleContext.assert); } moduleContext.tests.forEach((testContext) => { - const { name, ...targetContext } = module; - - Object.assign(testContext, targetContext); + const { name, ...moduleContextWithoutName } = moduleContext; - testContext.steps = moduleContext.steps; - testContext.totalExecutedAssertions = moduleContext.totalExecutedAssertions; - testContext.expectedAssertionCount = moduleContext.expectedAssertionCount; + Object.assign(testContext, moduleContextWithoutName, { + steps: moduleContext.steps, + totalExecutedAssertions: moduleContext.totalExecutedAssertions, + expectedAssertionCount: moduleContext.expectedAssertionCount, + }); }); }); _after(async () => { From 9ecd848fd195f1f3b7095ad84ffac16f5e460a7a Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Sat, 29 Jul 2023 19:56:17 +0200 Subject: [PATCH 6/7] First mature deno implementation for the tests --- shims/deno/assert.js | 142 +++++++++++++++++-------- shims/deno/index.js | 244 ++++++++++++++++++++++++++++++++++++++----- shims/node/assert.js | 3 - shims/node/index.js | 6 +- 4 files changed, 318 insertions(+), 77 deletions(-) diff --git a/shims/deno/assert.js b/shims/deno/assert.js index 0ab7938..aec686a 100644 --- a/shims/deno/assert.js +++ b/shims/deno/assert.js @@ -9,39 +9,79 @@ export class AssertionError extends DenoAssertionError { } } -// NOTE: Maybe do the expect, steps in some object, and also do timeout and async(?) -export default { - _steps: [], - timeout() { - return true; // NOTE: NOT implemented - }, - step(value = '') { - this._steps.push(value); - }, - verifySteps(steps, message = 'Verify steps failed!') { - const result = this.deepEqual(this._steps, steps, message); +export default class Assert { + AssertionError = AssertionError + + #asyncOps = []; + + constructor(module, test) { + this.test = test || module; + } + _incrementAssertionCount() { + this.test.totalExecutedAssertions++; + } + timeout(number) { + if (!Number.isInteger(number) || number < 0) { + throw new Error('assert.timeout() expects a positive integer.'); + } + + this.test.timeout = number; + } + step(message) { + let assertionMessage = message; + let result = !!message; + + this.test.steps.push(message); + + if (typeof message === 'undefined' || message === '') { + assertionMessage = 'You must provide a message to assert.step'; + } else if (typeof message !== 'string') { + assertionMessage = 'You must provide a string value to assert.step'; + result = false; + } - this._steps.length = 0; + this.pushResult({ + result, + message: assertionMessage + }); + } + verifySteps(steps, message = 'Verify steps failed!') { + this.deepEqual(this.test.steps, steps, message); + this.test.steps.length = 0; + } + expect(number) { + if (!Number.isInteger(number) || number < 0) { + throw new Error('assert.expect() expects a positive integer.'); + } - return result; - }, - expect() { - return () => {}; // NOTE: NOT implemented - }, + this.test.expectedAssertionCount = number; + } async() { - return () => {}; // NOTE: noop, node should have sanitizeResources - }, + let resolveFn; + let done = new Promise(resolve => { resolveFn = resolve; }); + + this.#asyncOps.push(done); + + return () => { resolveFn(); }; + } + async waitForAsyncOps() { + return Promise.all(this.#asyncOps); + } pushResult(resultInfo = {}) { - if (!result) { + this._incrementAssertionCount(); + if (!resultInfo.result) { throw new AssertionError({ actual: resultInfo.actual, expected: resultInfo.expected, - message: result.Infomessage || 'Custom assertion failed!', + message: resultInfo.message || 'Custom assertion failed!', stackStartFn: this.pushResult, }); } - }, + + return this; + } ok(state, message) { + this._incrementAssertionCount(); if (!state) { throw new AssertionError({ actual: state, @@ -50,8 +90,9 @@ export default { stackStartFn: this.ok, }); } - }, + } notOk(state, message) { + this._incrementAssertionCount(); if (state) { throw new AssertionError({ actual: state, @@ -60,8 +101,9 @@ export default { stackStartFn: this.notOk, }); } - }, + } true(state, message) { + this._incrementAssertionCount(); if (state !== true) { throw new AssertionError({ actual: state, @@ -70,8 +112,9 @@ export default { stackStartFn: this.true, }); } - }, + } false(state, message) { + this._incrementAssertionCount(); if (state !== false) { throw new AssertionError({ actual: state, @@ -80,8 +123,9 @@ export default { stackStartFn: this.false, }); } - }, + } equal(actual, expected, message) { + this._incrementAssertionCount(); if (actual != expected) { throw new AssertionError({ actual, @@ -91,8 +135,9 @@ export default { stackStartFn: this.equal, }); } - }, + } notEqual(actual, expected, message) { + this._incrementAssertionCount(); if (actual == expected) { throw new AssertionError({ actual, @@ -102,11 +147,12 @@ export default { stackStartFn: this.notEqual, }); } - }, + } propEqual(actual, expected, message) { + this._incrementAssertionCount(); let targetActual = objectValues(actual); let targetExpected = objectValues(expected); - if (!window.QUnit.equiv(targetActual, targetExpected)) { + if (!QUnit.equiv(targetActual, targetExpected)) { throw new AssertionError({ actual: targetActual, expected: targetExpected, @@ -114,11 +160,12 @@ export default { stackStartFn: this.propEqual, }); } - }, + } notPropEqual(actual, expected, message) { + this._incrementAssertionCount(); let targetActual = objectValues(actual); let targetExpected = objectValues(expected); - if (window.QUnit.equiv(targetActual, targetExpected)) { + if (QUnit.equiv(targetActual, targetExpected)) { throw new AssertionError({ actual: targetActual, expected: targetExpected, @@ -126,11 +173,12 @@ export default { stackStartFn: this.notPropEqual, }); } - }, + } propContains(actual, expected, message) { + this._incrementAssertionCount(); let targetActual = objectValuesSubset(actual, expected); let targetExpected = objectValues(expected, false); - if (!window.QUnit.equiv(targetActual, targetExpected)) { + if (!QUnit.equiv(targetActual, targetExpected)) { throw new AssertionError({ actual: targetActual, expected: targetExpected, @@ -138,11 +186,12 @@ export default { stackStartFn: this.propContains, }); } - }, + } notPropContains(actual, expected, message) { + this._incrementAssertionCount(); let targetActual = objectValuesSubset(actual, expected); let targetExpected = objectValues(expected); - if (window.QUnit.equiv(targetActual, targetExpected)) { + if (QUnit.equiv(targetActual, targetExpected)) { throw new AssertionError({ actual: targetActual, expected: targetExpected, @@ -150,9 +199,10 @@ export default { stackStartFn: this.notPropContains, }); } - }, + } deepEqual(actual, expected, message) { - if (!window.QUnit.equiv(actual, expected)) { + this._incrementAssertionCount(); + if (!QUnit.equiv(actual, expected)) { throw new AssertionError({ actual, expected, @@ -161,9 +211,10 @@ export default { stackStartFn: this.deepEqual, }); } - }, + } notDeepEqual(actual, expected, message) { - if (window.QUnit.equiv(actual, expected)) { + this._incrementAssertionCount(); + if (QUnit.equiv(actual, expected)) { throw new AssertionError({ actual, expected, @@ -172,8 +223,9 @@ export default { stackStartFn: this.notDeepEqual, }); } - }, + } strictEqual(actual, expected, message) { + this._incrementAssertionCount(); if (actual !== expected) { throw new AssertionError({ actual, @@ -183,8 +235,9 @@ export default { stackStartFn: this.strictEqual, }); } - }, + } notStrictEqual(actual, expected, message) { + this._incrementAssertionCount(); if (actual === expected) { throw new AssertionError({ actual, @@ -194,8 +247,9 @@ export default { stackStartFn: this.notStrictEqual, }); } - }, + } throws(blockFn, expectedInput, assertionMessage) { + this?._incrementAssertionCount(); let [expected, message] = validateExpectedExceptionArgs(expectedInput, assertionMessage, 'rejects'); if (typeof blockFn !== 'function') { throw new AssertionError({ @@ -228,8 +282,9 @@ export default { message: 'Function passed to `assert.throws` did not throw an exception!', stackStartFn: this.throws, }); - }, + } async rejects(promise, expectedInput, assertionMessage) { + this._incrementAssertionCount(); let [expected, message] = validateExpectedExceptionArgs(expectedInput, assertionMessage, 'rejects'); let then = promise && promise.then; if (typeof then !== 'function') { @@ -276,4 +331,3 @@ ${inspect(expected)}` function inspect(value) { return util.inspect(value, { depth: 10, colors: true, compact: false }); } - diff --git a/shims/deno/index.js b/shims/deno/index.js index 48c41c1..bfc4f91 100644 --- a/shims/deno/index.js +++ b/shims/deno/index.js @@ -1,48 +1,238 @@ import { - afterEach, - beforeEach, beforeAll, afterAll, describe, it, } from "https://deno.land/std@0.192.0/testing/bdd.ts"; -import assert from './assert.js'; +import Assert from './assert.js'; -// TODO: TEST beforeEach, before, afterEach, after, currently not sure if they work! -export const module = async function(moduleName, runtimeOptions, moduleContent) { - let targetRuntimeOptions = moduleContent ? Object.assign(runtimeOptions, { name: moduleName }) : { name: moduleName }; - let targetModuleContent = moduleContent ? moduleName : runtimeOptions; +class TestContext { + name; - return describe(assignDefaultValues(targetRuntimeOptions, { concurrency: true }), async function() { - return await targetModuleContent({ before: beforeAll, after: afterAll, beforeEach, afterEach }, { - moduleName, - options: runtimeOptions + #module; + get module() { + return this.#module; + } + set module(value) { + this.#module = value; + } + + #assert; + get assert() { + return this.#assert; + } + set assert(value) { + this.#assert = value; + } + + #timeout; + get timeout() { + return this.#timeout; + } + set timeout(value) { + this.#timeout = value; + } + + #steps = []; + get steps() { + return this.#steps; + } + set steps(value) { + this.#steps = value; + } + + #expectedAssertionCount; + get expectedAssertionCount() { + return this.#expectedAssertionCount; + } + set expectedAssertionCount(value) { + this.#expectedAssertionCount = value; + } + + #totalExecutedAssertions = 0; + get totalExecutedAssertions() { + return this.#totalExecutedAssertions; + } + set totalExecutedAssertions(value) { + this.#totalExecutedAssertions = value; + } + + constructor(name, moduleContext) { + if (moduleContext) { + this.name = `${moduleContext.name} | ${name}`; + this.module = moduleContext; + this.module.tests.push(this); + this.assert = new Assert(moduleContext, this); + } + } + + finish() { + if (this.totalExecutedAssertions === 0) { + this.assert.pushResult({ + result: false, + actual: this.totalExecutedAssertions, + expected: '> 0', + message: `Expected at least one assertion to be run for test: ${this.name}`, + }); + } else if (this.steps.length > 0) { + this.assert.pushResult({ + result: false, + actual: this.steps, + expected: [], + message: `Expected assert.verifySteps() to be called before end of test after using assert.step(). Unverified steps: ${this.steps.join(', ')}`, + }); + } else if (this.expectedAssertionCount && this.expectedAssertionCount !== this.totalExecutedAssertions) { + this.assert.pushResult({ + result: false, + actual: this.totalExecutedAssertions, + expected: this.expectedAssertionCount, + message: `Expected ${this.expectedAssertionCount} assertions, but ${this.totalExecutedAssertions} were run for test: ${this.name}`, + }); + } + } +} + +class ModuleContext extends TestContext { + static currentModuleChain = []; + + static get lastModule() { + return this.currentModuleChain[this.currentModuleChain.length - 1]; + } + + #tests = []; + get tests() { + return this.#tests; + } + + #beforeEachHooks = []; + get beforeEachHooks() { + return this.#beforeEachHooks; + } + + #afterEachHooks = []; + get afterEachHooks() { + return this.#afterEachHooks; + } + + #moduleChain = []; + get moduleChain() { + return this.#moduleChain; + } + set moduleChain(value) { + this.#moduleChain = value; + } + + constructor(name) { + super(name); + + let parentModule = ModuleContext.currentModuleChain[ModuleContext.currentModuleChain.length - 1]; + + ModuleContext.currentModuleChain.push(this); + + this.moduleChain = ModuleContext.currentModuleChain.slice(0); + this.name = parentModule ? `${parentModule.name} > ${name}` : name; + this.assert = new Assert(this); + } +} + +export const module = (moduleName, runtimeOptions, moduleContent) => { + let targetRuntimeOptions = moduleContent ? runtimeOptions : {}; + let targetModuleContent = moduleContent ? moduleContent : runtimeOptions; + let moduleContext = new ModuleContext(moduleName); + + return describe(moduleName, { concurrency: true, ...targetRuntimeOptions }, async function () { + let beforeHooks = []; + let afterHooks = []; + + beforeAll(async function () { + Object.assign(moduleContext, moduleContext.moduleChain.reduce((result, module) => { + const { name, ...moduleWithoutName } = module; + + return Object.assign(result, moduleWithoutName, { + steps: result.steps.concat(module.steps), + expectedAssertionCount: module.expectedAssertionCount + ? module.expectedAssertionCount + : result.expectedAssertionCount + }); + }, { steps: [], expectedAssertionCount: undefined })); + + for (let hook of beforeHooks) { + await hook.call(moduleContext, moduleContext.assert); + } + + moduleContext.tests.forEach((testContext) => { + const { name, ...moduleContextWithoutName } = moduleContext; + + Object.assign(testContext, moduleContextWithoutName, { + steps: moduleContext.steps, + totalExecutedAssertions: moduleContext.totalExecutedAssertions, + expectedAssertionCount: moduleContext.expectedAssertionCount, + }); + }); + }); + afterAll(async () => { + for (const assert of moduleContext.tests.map(testContext => testContext.assert)) { + await assert.waitForAsyncOps(); + } + + let targetContext = moduleContext.tests[moduleContext.tests.length - 1]; + for (let j = afterHooks.length - 1; j >= 0; j--) { + await afterHooks[j].call(targetContext, targetContext.assert); + } + + moduleContext.tests.forEach(testContext => testContext.finish()); }); + + targetModuleContent.call(moduleContext, { + before(beforeFn) { + return beforeHooks.push(beforeFn); + }, + beforeEach(beforeEachFn) { + return moduleContext.beforeEachHooks.push(beforeEachFn); + }, + afterEach(afterEachFn) { + return moduleContext.afterEachHooks.push(afterEachFn); + }, + after(afterFn) { + return afterHooks.push(afterFn); + } + }, { moduleName, options: runtimeOptions }); + + ModuleContext.currentModuleChain.pop(); }); } -export const test = async function(testName, runtimeOptions, testContent) { - let targetRuntimeOptions = testContent ? Object.assign(runtimeOptions, { name: testName }) : { name: testName }; +export const test = (testName, runtimeOptions, testContent) => { + let moduleContext = ModuleContext.lastModule; + if (!moduleContext) { + throw new Error(`Test '${testName}' called outside of module context.`); + } + + let targetRuntimeOptions = testContent ? runtimeOptions : {}; let targetTestContent = testContent ? testContent : runtimeOptions; + let context = new TestContext(testName, moduleContext); - return it(targetRuntimeOptions, async function() { - let metadata = { testName, options: targetRuntimeOptions, expectedTestCount: undefined }; - return await targetTestContent(assert, metadata); + return it(testName, { concurrency: true, ...targetRuntimeOptions }, async function () { + let result; + for (let module of context.module.moduleChain) { + for (let hook of module.beforeEachHooks) { + await hook.call(context, context.assert); + } + } - if (expectedTestCount) { + result = await targetTestContent.call(context, context.assert, { testName, options: runtimeOptions }); - } - }); -} + await context.assert.waitForAsyncOps(); -function assignDefaultValues(options, defaultValues) { - for (let key in defaultValues) { - if (options[key] === undefined) { - options[key] = defaultValues[key]; + for (let i = context.module.moduleChain.length - 1; i >= 0; i--) { + let module = context.module.moduleChain[i]; + for (let j = module.afterEachHooks.length - 1; j >= 0; j--) { + await module.afterEachHooks[j].call(context, context.assert); + } } - } - return options; + return result; + }); } -export default { module, test, assert }; +export default { module, test, config: {} }; diff --git a/shims/node/assert.js b/shims/node/assert.js index cd1e635..e2c0d85 100644 --- a/shims/node/assert.js +++ b/shims/node/assert.js @@ -6,7 +6,6 @@ import util from 'node:util'; export const AssertionError = _AssertionError; // More: contexts needed for timeout - // NOTE: QUnit API provides assert on hooks, which makes it hard to make it concurrent // NOTE: Another approach for a global report Make this._assertions.set(this.currentTest, (this._assertions.get(this.currentTest) || 0) + 1); for pushResult // NOTE: This should *always* be a singleton(?), passed around as an argument for hooks. Seems difficult with concurrency. Singleton needs to be a concurrent data structure. @@ -48,8 +47,6 @@ export default class Assert { }); } verifySteps(steps, message = 'Verify steps failed!') { - // const actualStepsClone = this.module.test.steps.slice(); // TODO: is this needed(?) - this.deepEqual(this.test.steps, steps, message); this.test.steps.length = 0; } diff --git a/shims/node/index.js b/shims/node/index.js index 00b9af7..d8db9a1 100644 --- a/shims/node/index.js +++ b/shims/node/index.js @@ -1,4 +1,4 @@ -import { describe, it, before as _before, after as _after, beforeEach as _beforeEach, afterEach as _afterEach } from 'node:test'; +import { describe, it, before as beforeAll, after as afterAll } from 'node:test'; import Assert from './assert.js'; // NOTE: node.js beforeEach & afterEach is buggy because the TestContext it has is NOT correct reference when called, it gets the last context @@ -143,7 +143,7 @@ export const module = (moduleName, runtimeOptions, moduleContent) => { let beforeHooks = []; let afterHooks = []; - _before(async function () { + beforeAll(async function () { Object.assign(moduleContext, moduleContext.moduleChain.reduce((result, module) => { const { name, ...moduleWithoutName } = module; @@ -169,7 +169,7 @@ export const module = (moduleName, runtimeOptions, moduleContent) => { }); }); }); - _after(async () => { + afterAll(async () => { for (const assert of moduleContext.tests.map(testContext => testContext.assert)) { await assert.waitForAsyncOps(); } From 9fd1ee38cdd20f6a7bcc1aff1b29f3cd31dfa3fd Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Sat, 29 Jul 2023 20:00:02 +0200 Subject: [PATCH 7/7] Pin to node v20.5 and add TODO --- Dockerfile | 2 +- TODO | 15 +++++++++++++++ package.json | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4d8cb1a..3af0275 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.4.0-slim +FROM node:20.5.0-slim RUN apt-get update \ && apt-get install -y curl unzip wget gnupg \ diff --git a/TODO b/TODO index e69de29..3e06b2a 100644 --- a/TODO +++ b/TODO @@ -0,0 +1,15 @@ +create runtime error for module with no tests, create runtime error for hooks declared in inner modules without hooks reference(typo) + +context parameter to module() and test() instead of this + +Test left: custom assertions(?)[pushResult], timeout + +Add .match() for pattern match + +interested in node.js doctool(?) + +function innerFail(obj) { + if (obj.message instanceof Error) throw obj.message; + + throw new AssertionError(obj); +} diff --git a/package.json b/package.json index 2421c21..4bad108 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "ts-node": ">=10.7.0" }, "volta": { - "node": "20.4.0" + "node": "20.5.0" }, "prettier": { "printWidth": 100,