Skip to content

Commit

Permalink
[MINOR] Allow Failing With Rejection Message On Async Tests
Browse files Browse the repository at this point in the history
  • Loading branch information
michasherman authored Sep 9, 2019
1 parent 76ea925 commit d0e88e8
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 60 deletions.
59 changes: 44 additions & 15 deletions dist/passable.js

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

2 changes: 1 addition & 1 deletion dist/passable.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/passable.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/passable.min.js.map

Large diffs are not rendered by default.

31 changes: 29 additions & 2 deletions docs/test/async.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,44 @@ test('name', 'must be unique', new Promise((resolve, reject) => {
} else {
resolve(); // completes. doesn't mark the test as failing
}
}
});
}));
```

## Returning a Promise / Async/Await

```js
test('name', 'Should be unique', async () => {
const res = await doesUserExist(user)
const res = await doesUserExist(user);
return res;
});

test('name', 'I fail', async () => Promise.reject());
```

## Rejecting with rejection message

What if your promise can reject with different messages? No problem!
You can reject the promise with your own message by passing it to the
rejection callback.

Notice that when using rejection messages we do not need to pass `statement`
argument to `test`. This means that the statement will always be inferred
from the rejection message.

In case you do pass `statement`, it will serve as a fallback message in any
case that the rejection message is not provided.

```js
test('name', new Promise((resolve, reject) => {
fetch(`/checkUsername?name=${name}`)
.then(res => res.json)
.then(data => {
if (data.status === 'fail') {
reject(data.message); // rejects with message and marks the test as failing
} else {
resolve(); // completes. doesn't mark the test as failing
}
});
}));
```
58 changes: 42 additions & 16 deletions src/core/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@ export const runAsync = (testPromise) => {
}
};

const fail = () => {
const fail = (rejectionMessage) => {
const message = typeof rejectionMessage === 'string'
? rejectionMessage
: statement;

if (parent.pending.includes(testPromise)) {
parent.result.fail(fieldName, statement, severity);
parent.result.fail(fieldName, message, severity);
}

done();
Expand Down Expand Up @@ -113,27 +117,49 @@ const register = (testFn) => {
}
};

/**
* Checks that a given argument qualifies as a test function
* @param {*} testFn
* @return {Boolean}
*/
const isTestFn = (testFn) => {
if (!testFn) {
return false;
}

return typeof testFn.then === 'function' || typeof testFn === 'function';
};

/**
* The function used by the consumer
* @param {String} fieldName name of the field to test against
* @param {String} statement the message shown to the user in case of a failure
* @param {String} [statement] the message shown to the user in case of a failure
* @param {function | Promise} testFn the actual test callback or promise
* @param {String} Severity indicates whether the test should fail or warn
* @param {String} [severity] indicates whether the test should fail or warn
*/
const test = (fieldName, statement, testFn, severity) => {
if (!testFn) {
return;
const test = (fieldName, ...args) => {
let statement,
testFn,
severity;

if (typeof args[0] === 'string') {
[statement, testFn, severity] = args;
} else if (isTestFn(args[0])) {
[testFn, severity] = args;
}
if (typeof testFn.then === 'function' || typeof testFn === 'function') {
Object.assign(testFn, {
fieldName,
statement,
severity,
parent: ctx.parent
});

register(testFn);

if (!isTestFn(testFn)) {
return;
}

Object.assign(testFn, {
fieldName,
statement,
severity,
parent: ctx.parent
});

register(testFn);
};

export default test;
89 changes: 65 additions & 24 deletions src/core/test/spec.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import passable from '../passable';
import test from '.';
import ctx from '../context';
import { expect } from 'chai';
import { WARN, FAIL } from '../../index';
import { noop, random, sample, clone } from 'lodash';
import { FAIL, WARN } from '../../index';
import { clone, noop, random } from 'lodash';
import { lorem } from 'faker';
import sinon from 'sinon';

Expand Down Expand Up @@ -87,81 +86,123 @@ describe('Test Passables "test" function', () => {
describe('Test is async', () => {

describe('When returning promise to test callback', () => {
let f1, f2, f3, f4, output;
let f1, f2, f3, f4, f5, f6, output;
const rejectionMessage = lorem.sentence();

beforeEach(() => {
f1 = lorem.word();
f2 = lorem.word();
f3 = lorem.word();
f4 = lorem.word();
f5 = lorem.word();
f6 = lorem.word();

const rejectLater = () => new Promise((res, rej) => {
setTimeout(rej, 500);
});

output = passable(lorem.word(), (test) => {
test(f1, lorem.sentence(), () => Promise.reject());
test(f2, lorem.sentence(), () => new Promise((resolve, reject) => {
test(f1, f1, () => Promise.reject());
test(f2, f2, () => new Promise((resolve, reject) => {
setTimeout(reject, 200);
}));
test(f3, lorem.sentence(), () => new Promise((resolve) => {
test(f3, () => Promise.reject(lorem.word()));
test(f3, () => Promise.reject(rejectionMessage));
test(f4, f4, () => Promise.reject(rejectionMessage));
test(f5, f5, () => new Promise((resolve) => {
setTimeout(resolve, 100);
}));
test(f4, lorem.sentence(), async() => await rejectLater());
test(f6, f6, async() => await rejectLater());
});
});

it('Should fail for rejected promise', (done) => {
expect(output.hasErrors(f1)).to.equal(false);
expect(output.hasErrors(f2)).to.equal(false);
expect(output.hasErrors(f4)).to.equal(false);
[f1, f2, f3, f4, f6].forEach((field) =>
expect(output.hasErrors(field)).to.equal(false)
);

setTimeout(() => {
[f1, f2, f3, f4, f6].forEach((field) =>
expect(output.hasErrors(field)).to.equal(true)
);

[f1, f2, f6].forEach((field) =>
expect(output.getErrors(field)).to.include(field)
);

done();
}, 550);
});

it('Should fail with rejection message when provided', (done) => {
setTimeout(() => {
expect(output.hasErrors(f1)).to.equal(true);
expect(output.hasErrors(f2)).to.equal(true);
expect(output.hasErrors(f4)).to.equal(true);
[f3, f4].forEach((field) => {
expect(output.getErrors(field)).to.include(rejectionMessage);
});

done();
}, 550);
});

it('Should pass for fulfilled promises', (done) => {
expect(output.hasErrors(f3)).to.equal(false);
expect(output.hasErrors(f5)).to.equal(false);

setTimeout(() => {
expect(output.hasErrors(f3)).to.equal(false);
expect(output.hasErrors(f5)).to.equal(false);
done();
}, 500);
});

});

describe('When passing a Promise as a test', () => {
describe('failing', () => {
let f1, f2;
let f1, f2, f3, f4;
const rejectionMessage = lorem.sentence();

beforeEach(() => {
f1 = lorem.word();
f2 = lorem.word();
f3 = lorem.word();
f4 = lorem.word();

output = passable(lorem.word(), (test, draft) => {
test(f1, lorem.sentence(), new Promise((resolve, reject) => setImmediate(reject)));
test(f2, lorem.sentence(), new Promise((resolve) => setTimeout(resolve)));
test(f2, lorem.sentence(), new Promise((resolve) => setTimeout(resolve, 500)));
output = passable(lorem.word(), (test) => {
test(f1, f1, new Promise((_, reject) => setImmediate(reject)));
test(f2, new Promise((_, reject) => setImmediate(() => reject(rejectionMessage))));
test(f3, lorem.sentence(), new Promise((_, reject) => setImmediate(() => reject(lorem.word()))));
test(f3, f3, new Promise((_, reject) => setImmediate(() => reject(rejectionMessage))));
test(f4, f4, new Promise((resolve) => setTimeout(resolve)));
test(f4, f4, new Promise((resolve) => setTimeout(resolve, 500)));
test(lorem.word(), lorem.sentence(), noop);
});
});

it('Should immediately register tests', () => {
expect(output.testCount).to.equal(4);
expect(output.testCount).to.equal(7);
});

it('Should run async test promise', (done) => {
passable(lorem.word(), (test) => {
test(f1, lorem.sentence(), new Promise((resolve) => done()));
test(f1, lorem.sentence(), new Promise(() => done()));
test(lorem.word(), lorem.sentence(), noop);
});
});

it('Should fail with rejection message when provided', (done) => {
setTimeout(() => {
[f2, f3].forEach((field) => {
expect(output.getErrors(field)).to.include(rejectionMessage);
});

done();
}, 550);
});

it('Should only mark test as failing after rejection', (done) => {
expect(output.failCount).to.equal(0);

setTimeout(() => {
expect(output.failCount).to.equal(1);
expect(output.failCount).to.equal(4);
done();
}, 10);
});
Expand Down

0 comments on commit d0e88e8

Please sign in to comment.