Skip to content

Commit

Permalink
Add around hook
Browse files Browse the repository at this point in the history
Effectively allowing beforeEach and afterEach hooks to modify the same
data without having to set intermediate variables.

The most obvious use case is resetting global (or environment) variables
which need to be in specific states for the tests, but should be
reverted after the fact.
  • Loading branch information
HookyQR committed Mar 23, 2020
1 parent 93f55af commit 56b834c
Show file tree
Hide file tree
Showing 12 changed files with 241 additions and 18 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Expand Up @@ -3,6 +3,7 @@ language: node_js
node_js:
- 10
- 11
- 12
- lts/*

os:
Expand Down
1 change: 1 addition & 0 deletions TODO
Expand Up @@ -15,6 +15,7 @@ Run:
✔ After Hooks @done(19-07-12 22:19)
✔ AfterEach @done(19-07-15 17:35)
✔ BeforeEach @done(19-07-15 17:35)
✔ AroundEach @done(20-03-23 17:48)
✔ SharedContext @done(19-07-19 14:47)
✔ SharedExample @done(19-07-16 23:57)
✔ It - standard @done(19-06-30 21:10)
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
@@ -1,10 +1,10 @@
{
"name": "@jsspec/jsspec",
"version": "0.1.5",
"version": "0.2.0",
"description": "JSSpec - contextualised test runner for javascript",
"main": "index.js",
"scripts": {
"test": "c8 -r html -r text ./bin/jsspec -r chai/register-expect"
"test": "c8 ./bin/jsspec -r chai/register-expect"
},
"bin": "bin/jsspec",
"repository": {
Expand Down
141 changes: 141 additions & 0 deletions spec/around_hook.spec.js
@@ -0,0 +1,141 @@
'use strict';

const { nonExecutor, callTrace } = require('./spec_helper');

describe('aroundEach', () => {
set('trace', callTrace);

describe('call types and ordering', { random: false }, () => {
const afterTrace = callTrace();

aroundEach('Named with option', {}, async (eg) => {
afterTrace('A'); await eg(); afterTrace('B');
});
aroundEach('Named', async (eg) => {
afterTrace('C'); await eg(); afterTrace('D');
});
aroundEach(async (eg) => {
afterTrace('E'); await eg(); afterTrace('F');
});

it('all are called in order, once per example', () => expect(afterTrace('x')).to.eq('ACEx'));
it('all are called in order, once per example', () => expect(afterTrace('y')).to.eq('ACExFDBACEy'));
after(() => expect(afterTrace()).to.eq('ACExFDBACEyFDB'))
});

describe('calling with no function', () => {
try {
aroundEach('what even is this');
it(nonExecutor);
}catch (error) {
it('throws', () => expect(error).to.be.an.instanceOf(TypeError));
}
});

describe('calling from within an example', () => {
it('throws', () => expect(
() => aroundEach('called in executor', nonExecutor)
).to.throw(ReferenceError, 'A hook (`aroundEach`) can not be defined inside an example block'));
});

context('passed a generator', () => {
set('endState', 'acb');

aroundEach(function* () {
trace('a');
yield;
trace('b');

expect(trace()).to.eq(endState);
});

it('wraps correctly', () => {
expect(trace('c')).to.eq('ac');
});

describe('at depth', () => {
set('endState', 'adfeb');

aroundEach(function* () {
trace('d');
yield;
trace('e');

expect(trace()).to.eq('adfe');
});

it('wraps correctly', () => {
expect(trace('f')).to.eq('adf');
});
});
});

context('passed an async generator', () => {
set('endState', 'gih');

aroundEach(async function* () {
trace('g');
yield;
trace('h');

expect(trace()).to.eq(endState);
});

it('wraps correctly', () => {
expect(trace('i')).to.eq('gi');
});
});

context('passed an regular method (example as promise)', () => {
set('endState', 'jlk');

aroundEach(example => {
trace('j');
return example().then(() => {
trace('k');
expect(trace()).to.eq(endState);
});
});

it('wraps correctly', () => {
expect(trace('l')).to.eq('jl');
});
});

context('passed an async method', () => {
set('endState', 'mon');

aroundEach(async example => {
trace('m');
await example();
trace('n');
expect(trace()).to.eq(endState);
});

it('wraps correctly', () => {
expect(trace('o')).to.eq('mo');
});
});

describe('ordering of hooks', () => {
const afterTrace = callTrace();

set('endState', 'ptqvrus');

before(() => afterTrace('p'));
beforeEach(() => afterTrace('q'));
afterEach(() => afterTrace('r'));
after(() => {
expect(afterTrace('s')).to.eq(endState);
});
aroundEach(function * () {
afterTrace('t');
yield;
afterTrace('u');
});

it('enters in order', () => expect(afterTrace('v')).to.eq('ptqv'));


});
});
2 changes: 1 addition & 1 deletion spec/hooks.spec.js
@@ -1,6 +1,6 @@
'use strict';
const { nonExecutor, callTrace, noOp } = require('./spec_helper');

const { nonExecutor, callTrace, noOp } = require('./spec_helper');

describe('hooks', () => {
context('hooks only run if there is an example', () => {
Expand Down
1 change: 1 addition & 0 deletions src/context.js
Expand Up @@ -23,6 +23,7 @@ class RootContext {
retrieveCreator(key) { throw ReferenceError(`\`${key}\` is not set in this context`); }
findSharedContext() { return null; }
findExamples() { return null; }
wrapAroundEach(example) { return example; }
}

const rootContext = new RootContext();
Expand Down
36 changes: 36 additions & 0 deletions src/example_wrapper.js
@@ -0,0 +1,36 @@
'use strict';

const Example = require('./example');

/* c8 ignore next 2 */
const Generator = (function* () { }).constructor;
const AsyncGenerator = (async function* () { }).constructor;

class ExampleWrapper extends Example {
around(example) {
this.child = example;
return this;
}

async runGenerator() {
const runner = this.block();
let partial = runner.next();
await this.child.run();
if (!partial.done) { partial = runner.next(); }
}

async runAsyncGenerator() {
const runner = this.block();
let partial = await runner.next();
await this.child.run();
if (!partial.done) { partial = await runner.next(); }
}

async run() {
if (this.block instanceof Generator) await this.runGenerator();
else if (this.block instanceof AsyncGenerator) await this.runAsyncGenerator();
else await this.block(async () => await this.child.run());
}
}

module.exports = ExampleWrapper;
1 change: 1 addition & 0 deletions src/expose_global.js
Expand Up @@ -18,6 +18,7 @@ const execution = {
};

const executionHook = {
aroundEach: false,
beforeEach: false,
afterEach: false,
before: false,
Expand Down
49 changes: 49 additions & 0 deletions src/global/aroundEach.js
@@ -0,0 +1,49 @@
const filterStack = require('../filter_stack');
const ExampleWrapper = require('../example_wrapper');

module.exports = {
initialise() {
this.aroundEach = [];
},
instance: {
addAroundEach(exampleWrapper) {
this.aroundEach.push(exampleWrapper);
},

wrapAroundEach(hookedExample) {
let wrapped = this.aroundEach.reduceRight((inner,wrapper) => wrapper.around(inner), hookedExample);
return this.parent.wrapAroundEach(wrapped);
},

hookedExample(example) {
return {
run: async () => {
const storeFailure = error => example.failure = example.failure || filterStack(error);

await this.runBeforeEach().catch(storeFailure);
if (!example.failure) {
await example.run().catch(storeFailure);
await this.runAfterEach().catch(storeFailure);
}
}
};
},

runAroundEach: async function(example) {
let wrapped = this.wrapAroundEach(this.hookedExample(example));

await wrapped.run().catch(error => example.failure = example.failure || filterStack(error));
}
},
global(description, optionOrBlock, block) {
if (this.executing) throw new ReferenceError('A hook (`aroundEach`) can not be defined inside an example block');

if (block instanceof Function) { /* noop */ }
else if (optionOrBlock instanceof Function) [optionOrBlock, block] = [{}, optionOrBlock];
else if ( description instanceof Function) [description, optionOrBlock, block] = ['', {}, description];
else throw TypeError('`aroundEach` must be provided an executable block');

this.currentContext.addAroundEach(new ExampleWrapper(description, 'aroundEach', optionOrBlock, block, this.currentContext));

}
};
14 changes: 1 addition & 13 deletions src/global/it.js
@@ -1,8 +1,6 @@
const Example = require('../example');
const filterStack = require('../filter_stack');

const noOp = () => undefined;

module.exports = {
initialise() {
this.examples = [];
Expand All @@ -24,21 +22,11 @@ module.exports = {
this.executing = state;
this.parent.setTreeExecution(state);
},

async hookedExample(example) {
const storeFailure = error => example.failure = example.failure || filterStack(error);

await this.runBeforeEach().catch(storeFailure);
if (!example.failure) {
await example.run().catch(storeFailure);
await this.runAfterEach().catch(storeFailure);
}
},
async runExample(example) {
this.startBlock();
this.emitter.emit('exampleStart', example);
await this.runBeforeHooks().catch(error => example.failure = filterStack(error));
await this.hookedExample(example);
if (!example.failure) await this.runAroundEach(example);

this.emitter.emit('exampleEnd', example);
this.endBlock();
Expand Down
7 changes: 6 additions & 1 deletion src/walker.js
Expand Up @@ -30,7 +30,12 @@ class FileDetail {
}

const nameToDetail = name => ({name});
const uniqueDetails = names => Array.from(new Set(names.flat()), nameToDetail);

const addAll = (collected, items = []) => [...collected, ...items];
const flat = results => results.reduce(addAll, []);

const uniqueDetails = names => Array.from(new Set(flat(names)), nameToDetail);


class Walker {
constructor(globOrFileList, random) {
Expand Down

0 comments on commit 56b834c

Please sign in to comment.