Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core: Add QUnit.hooks to globally add beforeEach and afterEach #1673

Merged
merged 1 commit into from
Feb 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions docs/QUnit/hooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
layout: page-api
title: QUnit.hooks
excerpt: Add global callbacks to run before or after each test.
groups:
- main
version_added: "unreleased"
---

`QUnit.hooks.beforeEach( callback )`<br>
`QUnit.hooks.afterEach( callback )`

Register a global callback to run before or after each test.

| parameter | description |
|-----------|-------------|
| callback (function) | Callback to execute. Called with an [assert](../assert/index.md) argument. |

This is the equivalent of applying a `QUnit.module()` hook to all modules and all tests, including global tests that are not associated with any module.

Similar to module hooks, global hooks support async functions or returning a Promise, which will be waited for before QUnit continues executing tests. Each global hook also has access to the same `assert` object and test context as the [QUnit.test](./test.md) that the hook is running for.

For more details about hooks, refer to [QUnit.module § Hooks](./module.md#hooks).

## Examples

```js
QUnit.hooks.beforeEach( function() {
this.app = new MyApp();
});

QUnit.hooks.afterEach( async function( assert ) {
assert.deepEqual( [], await this.app.getErrors(), "MyApp errors" );

MyApp.reset();
});
```
4 changes: 2 additions & 2 deletions docs/QUnit/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ You can use hooks to prepare fixtures, or run other setup and teardown logic. Ho
* `afterEach`: Run a callback after each test.
* `after`: Run a callback after the last test.

You can add hooks via the `hooks` parameter of a [scoped module](#nested-scope), or in the module [`options`](#options-object) object.
You can add hooks via the `hooks` parameter of a [scoped module](#nested-scope), or in the module [`options`](#options-object) object, or globally for all tests via [QUnit.hooks](./hooks.md).

Hooks that are added to a module, will also apply to tests in any nested modules.

Hooks that run _before_ a test, are ordered from outer-most to inner-most, in the order that they are added. This means that a test will first run the hooks of parent modules, and then the hooks added to the immediate module the test is a part of. Hooks that run _after_ a test, are ordered from inner-most to outer-most, in the reverse order. In other words, `before` and `beforeEach` callbacks form a [queue][], while `afterEach` and `after` form a [stack][].
Hooks that run _before_ a test, are ordered from outer-most to inner-most, in the order that they are added. This means that a test will first run any global beforeEach hooks, then the hooks of parent modules, and finally the hooks added to the immediate module the test is a part of. Hooks that run _after_ a test, are ordered from inner-most to outer-most, in the reverse order. In other words, `before` and `beforeEach` callbacks form a [queue][], while `afterEach` and `after` form a [stack][].

[queue]: https://en.wikipedia.org/wiki/Queue_%28abstract_data_type%29
[stack]: https://en.wikipedia.org/wiki/Stack_%28abstract_data_type%29
Expand Down
2 changes: 2 additions & 0 deletions src/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import exportQUnit from "./export";
import reporters from "./reporters";

import config from "./core/config";
import { hooks } from "./core/hooks";
import { extend, objectType, is, now } from "./core/utilities";
import { registerLoggingCallbacks, runLoggingCallbacks } from "./core/logging";
import { sourceFromStacktrace } from "./core/stacktrace";
Expand Down Expand Up @@ -44,6 +45,7 @@ extend( QUnit, {
dump,
equiv,
reporters,
hooks,
is,
objectType,
on,
Expand Down
4 changes: 4 additions & 0 deletions src/core/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ const config = {
}
},

// Exposed to make resets easier
// Ref https://github.com/qunitjs/qunit/pull/1598
globalHooks: {},
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

\cc @izelnakri FYI 🙂

I wasn't sure whether to expose it but at least until we have a more encapsulated way of instantiating a QUnit runner, this should help with your work in qunitx.


callbacks: {},

// The storage module to use for reordering tests
Expand Down
15 changes: 15 additions & 0 deletions src/core/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import config from "./config";

function makeAddGlobalHook( hookName ) {
return function addGlobalHook( callback ) {
if ( !config.globalHooks[ hookName ] ) {
config.globalHooks[ hookName ] = [];
}
config.globalHooks[ hookName ].push( callback );
};
}

export const hooks = {
beforeEach: makeAddGlobalHook( "beforeEach" ),
afterEach: makeAddGlobalHook( "afterEach" )
};
51 changes: 51 additions & 0 deletions src/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Promise from "./promise";
import config from "./core/config";
import {
diff,
errorString,
extend,
generateHash,
hasOwn,
Expand Down Expand Up @@ -224,6 +225,34 @@ Test.prototype = {
checkPollution();
},

queueGlobalHook( hook, hookName ) {
const runHook = () => {
config.current = this;

let promise;

if ( config.notrycatch ) {
promise = hook.call( this.testEnvironment, this.assert );

} else {
try {
promise = hook.call( this.testEnvironment, this.assert );
} catch ( error ) {
this.pushFailure(
"Global " + hookName + " failed on " + this.testName +
": " + errorString( error ),
extractStacktrace( error, 0 )
);
return;
}
}

this.resolvePromise( promise, hookName );
};

return runHook;
},

queueHook( hook, hookName, hookOwner ) {
const callHook = () => {
const promise = hook.call( this.testEnvironment, this.assert );
Expand Down Expand Up @@ -253,6 +282,14 @@ Test.prototype = {
return;
}
try {

// This try-block includes the indirect call to resolvePromise, which shouldn't
// have to be inside try-catch. But, since we support any user-provided thenable
// object, the thenable might throw in some unexpected way.
// This subtle behaviour is undocumented. To avoid new failures in minor releases
// we will not change this until QUnit 3.
// TODO: In QUnit 3, reduce this try-block to just hook.call(), matching
// the simplicity of queueGlobalHook.
callHook();
} catch ( error ) {
this.pushFailure( hookName + " failed on " + this.testName + ": " +
Expand All @@ -267,6 +304,19 @@ Test.prototype = {
hooks( handler ) {
const hooks = [];

function processGlobalhooks( test ) {
if (
( handler === "beforeEach" || handler === "afterEach" ) &&
config.globalHooks[ handler ]
) {
for ( let i = 0; i < config.globalHooks[ handler ].length; i++ ) {
hooks.push(
test.queueGlobalHook( config.globalHooks[ handler ][ i ], handler )
);
}
}
}

function processHooks( test, module ) {
if ( module.parentModule ) {
processHooks( test, module.parentModule );
Expand All @@ -281,6 +331,7 @@ Test.prototype = {

// Hooks are ignored on skipped tests
if ( !this.skip ) {
processGlobalhooks( this );
processHooks( this, this.module );
}

Expand Down
67 changes: 67 additions & 0 deletions test/cli/cli-main.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,73 @@ CALLBACK: done`;
assert.equal( execution.code, 0 );
} );

QUnit.test( "global hooks order", async assert => {
const expected = `
HOOK: A1 @ global beforeEach-1
HOOK: A1 @ global beforeEach-2
HOOK: A1 @ global afterEach-2
HOOK: A1 @ global afterEach-1
HOOK: B1 @ B before
HOOK: B1 @ global beforeEach-1
HOOK: B1 @ global beforeEach-2
HOOK: B1 @ B beforeEach
HOOK: B1 @ B afterEach
HOOK: B1 @ global afterEach-2
HOOK: B1 @ global afterEach-1
HOOK: B2 @ global beforeEach-1
HOOK: B2 @ global beforeEach-2
HOOK: B2 @ B beforeEach
HOOK: B2 @ B afterEach
HOOK: B2 @ global afterEach-2
HOOK: B2 @ global afterEach-1
HOOK: BC1 @ BC before
HOOK: BC1 @ global beforeEach-1
HOOK: BC1 @ global beforeEach-2
HOOK: BC1 @ B beforeEach
HOOK: BC1 @ BC beforeEach
HOOK: BC1 @ BC afterEach
HOOK: BC1 @ B afterEach
HOOK: BC1 @ global afterEach-2
HOOK: BC1 @ global afterEach-1
HOOK: BC2 @ global beforeEach-1
HOOK: BC2 @ global beforeEach-2
HOOK: BC2 @ B beforeEach
HOOK: BC2 @ BC beforeEach
HOOK: BC2 @ BC afterEach
HOOK: BC2 @ B afterEach
HOOK: BC2 @ global afterEach-2
HOOK: BC2 @ global afterEach-1
HOOK: BCD1 @ BCD before
HOOK: BCD1 @ global beforeEach-1
HOOK: BCD1 @ global beforeEach-2
HOOK: BCD1 @ B beforeEach
HOOK: BCD1 @ BC beforeEach
HOOK: BCD1 @ BCD beforeEach
HOOK: BCD1 @ BCD afterEach
HOOK: BCD1 @ BC afterEach
HOOK: BCD1 @ B afterEach
HOOK: BCD1 @ global afterEach-2
HOOK: BCD1 @ global afterEach-1
HOOK: BCD1 @ BCD after
HOOK: BCD1 @ BC after
HOOK: BCD1 @ B after`;

const command = "qunit hooks-global-order.js";
const execution = await execute( command );

assert.equal( execution.stderr, expected.trim() );
assert.equal( execution.code, 0 );
} );

QUnit.test( "global hooks context", async assert => {
const command = "qunit hooks-global-context.js";
const execution = await execute( command );

assert.equal( execution.code, 0 );
assert.equal( execution.stderr, "" );
assert.equal( execution.stdout, expectedOutput[ command ] );
} );

if ( semver.gte( process.versions.node, "12.0.0" ) ) {
QUnit.test( "run ESM test suite with import statement", async assert => {
const command = "qunit ../../es2018/esm.mjs";
Expand Down
11 changes: 11 additions & 0 deletions test/cli/fixtures/expected/tap-outputs.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,17 @@ ok 2 timeout > second
# todo 0
# fail 1`,

"qunit hooks-global-context.js":
`TAP version 13
ok 1 A > A1
ok 2 A > AB > AB1
ok 3 B
1..3
# pass 3
# skip 0
# todo 0
# fail 0`,

"qunit zero-assertions.js":
`TAP version 13
ok 1 Zero assertions > has a test
Expand Down
49 changes: 49 additions & 0 deletions test/cli/fixtures/hooks-global-context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
QUnit.hooks.beforeEach( function() {
this.x = 1;
this.fromGlobal = true;
} );
QUnit.hooks.afterEach( function() {
this.x = 100;
} );

QUnit.module( "A", function( hooks ) {
hooks.beforeEach( function() {
this.x = 2;
this.fromModule = true;
} );
hooks.afterEach( function() {
this.x = 20;
} );

QUnit.test( "A1", function( assert ) {
assert.equal( this.x, 2 );
assert.strictEqual( this.fromGlobal, true );
assert.strictEqual( this.fromModule, true );
assert.strictEqual( this.fromNested, undefined );
} );

QUnit.module( "AB", function( hooks ) {
hooks.beforeEach( function() {
this.x = 3;
this.fromNested = true;
} );
hooks.afterEach( function() {
this.x = 30;
} );

QUnit.test( "AB1", function( assert ) {
assert.strictEqual( this.x, 3 );
assert.strictEqual( this.fromGlobal, true );
assert.strictEqual( this.fromModule, true );
assert.strictEqual( this.fromNested, true );
} );
} );
} );

QUnit.test( "B", function( assert ) {
assert.strictEqual( this.x, 1 );
assert.strictEqual( this.fromGlobal, true );
assert.strictEqual( this.fromModule, undefined );
assert.strictEqual( this.fromNested, undefined );
} );

56 changes: 56 additions & 0 deletions test/cli/fixtures/hooks-global-order.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
function callback( label ) {
return function() {
console.warn( `HOOK: ${QUnit.config.current.testName} @ ${label}` );
};
}

QUnit.hooks.beforeEach( callback( "global beforeEach-1" ) );
QUnit.hooks.beforeEach( callback( "global beforeEach-2" ) );
QUnit.hooks.afterEach( callback( "global afterEach-1" ) );
QUnit.hooks.afterEach( callback( "global afterEach-2" ) );

QUnit.test( "A1", assert => {
assert.true( true );
} );

QUnit.module( "B", hooks => {
hooks.before( callback( "B before" ) );
hooks.beforeEach( callback( "B beforeEach" ) );
hooks.afterEach( callback( "B afterEach" ) );
hooks.after( callback( "B after" ) );

QUnit.test( "B1", assert => {
assert.true( true );
} );

QUnit.test( "B2", assert => {
assert.true( true );
} );

QUnit.module( "BC", hooks => {
hooks.before( callback( "BC before" ) );
hooks.beforeEach( callback( "BC beforeEach" ) );
hooks.afterEach( callback( "BC afterEach" ) );
hooks.after( callback( "BC after" ) );

QUnit.test( "BC1", assert => {
assert.true( true );
} );

QUnit.test( "BC2", assert => {
assert.true( true );
} );

QUnit.module( "BCD", hooks => {
hooks.before( callback( "BCD before" ) );
hooks.beforeEach( callback( "BCD beforeEach" ) );
hooks.afterEach( callback( "BCD afterEach" ) );
hooks.after( callback( "BCD after" ) );

QUnit.test( "BCD1", assert => {
assert.true( true );
} );
} );
} );
} );