Added a basic set of async matchers

- Fixes #1447
- Fixes #1547
This commit is contained in:
Steve Gravrock
2018-06-04 21:01:22 -07:00
parent 8f7327cb4d
commit 9472df0db4
16 changed files with 876 additions and 34 deletions

View File

@@ -60,6 +60,7 @@ var getJasmineRequireObj = (function (jasmineGlobal) {
j$.StackTrace = jRequire.StackTrace(j$);
j$.ExceptionFormatter = jRequire.ExceptionFormatter(j$);
j$.Expectation = jRequire.Expectation();
j$.AsyncExpectation = jRequire.AsyncExpectation(j$);
j$.buildExpectationResult = jRequire.buildExpectationResult();
j$.JsApiReporter = jRequire.JsApiReporter();
j$.matchersUtil = jRequire.matchersUtil(j$);
@@ -260,7 +261,7 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) {
};
j$.isPromise = function(obj) {
return typeof jasmineGlobal.Promise !== 'undefined' && obj.constructor === jasmineGlobal.Promise;
return typeof jasmineGlobal.Promise !== 'undefined' && obj && obj.constructor === jasmineGlobal.Promise;
};
j$.fnNameFor = function(func) {
@@ -536,6 +537,7 @@ getJasmineRequireObj().util = function(j$) {
getJasmineRequireObj().Spec = function(j$) {
function Spec(attrs) {
this.expectationFactory = attrs.expectationFactory;
this.asyncExpectationFactory = attrs.asyncExpectationFactory;
this.resultCallback = attrs.resultCallback || function() {};
this.id = attrs.id;
this.description = attrs.description || '';
@@ -592,6 +594,10 @@ getJasmineRequireObj().Spec = function(j$) {
return this.expectationFactory(actual, this);
};
Spec.prototype.expectAsync = function(actual) {
return this.asyncExpectationFactory(actual, this);
};
Spec.prototype.execute = function(onComplete, excluded) {
var self = this;
@@ -882,6 +888,19 @@ getJasmineRequireObj().Env = function(j$) {
}
};
var asyncExpectationFactory = function(actual, spec) {
return j$.AsyncExpectation.factory({
util: j$.matchersUtil,
customEqualityTesters: runnableResources[spec.id].customEqualityTesters,
actual: actual,
addExpectationResult: addExpectationResult
});
function addExpectationResult(passed, result) {
return spec.addExpectationResult(passed, result);
}
};
var defaultResourcesForRunnable = function(id, parentRunnableId) {
var resources = {spies: [], customEqualityTesters: [], customMatchers: {}, customSpyStrategies: {}};
@@ -1012,6 +1031,7 @@ getJasmineRequireObj().Env = function(j$) {
id: getNextSuiteId(),
description: 'Jasmine__TopLevel__Suite',
expectationFactory: expectationFactory,
asyncExpectationFactory: asyncExpectationFactory,
expectationResultFactory: expectationResultFactory
});
defaultResourcesForRunnable(topSuite.id);
@@ -1286,6 +1306,7 @@ getJasmineRequireObj().Env = function(j$) {
description: description,
parentSuite: currentDeclarationSuite,
expectationFactory: expectationFactory,
asyncExpectationFactory: asyncExpectationFactory,
expectationResultFactory: expectationResultFactory,
throwOnExpectationFailure: throwOnExpectationFailure
});
@@ -1379,6 +1400,7 @@ getJasmineRequireObj().Env = function(j$) {
id: getNextSpecId(),
beforeAndAfterFns: beforeAndAfterFns(suite),
expectationFactory: expectationFactory,
asyncExpectationFactory: asyncExpectationFactory,
resultCallback: specResultCallback,
getSpecName: function(spec) {
return getSpecName(spec, suite);
@@ -1460,6 +1482,14 @@ getJasmineRequireObj().Env = function(j$) {
return currentRunnable().expect(actual);
};
this.expectAsync = function(actual) {
if (!currentRunnable()) {
throw new Error('\'expectAsync\' was used when there was no current spec, this could be because an asynchronous test timed out');
}
return currentRunnable().expectAsync(actual);
};
this.beforeEach = function(beforeEachFunction, timeout) {
ensureIsNotNested('beforeEach');
ensureIsFunctionOrAsync(beforeEachFunction, 'beforeEach');
@@ -1950,6 +1980,160 @@ getJasmineRequireObj().Truthy = function(j$) {
return Truthy;
};
getJasmineRequireObj().AsyncExpectation = function(j$) {
var promiseForMessage = {
jasmineToString: function() { return 'a promise'; }
};
/**
* Asynchronous matchers.
* @namespace async-matchers
*/
function AsyncExpectation(options) {
var global = options.global || j$.getGlobal();
this.util = options.util || { buildFailureMessage: function() {} };
this.customEqualityTesters = options.customEqualityTesters || [];
this.addExpectationResult = options.addExpectationResult || function(){};
this.actual = options.actual;
this.isNot = options.isNot;
if (!global.Promise) {
throw new Error('expectAsync is unavailable because the environment does not support promises.');
}
if (!j$.isPromise(this.actual)) {
throw new Error('Expected expectAsync to be called with a promise.');
}
['toBeResolved', 'toBeRejected', 'toBeResolvedTo'].forEach(wrapCompare.bind(this));
}
function wrapCompare(name) {
var compare = this[name];
this[name] = function() {
var self = this;
var args = Array.prototype.slice.call(arguments);
args.unshift(this.actual);
// Capture the call stack here, before we go async, so that it will
// contain frames that are relevant to the user instead of just parts
// of Jasmine.
var errorForStack = j$.util.errorWithStack();
return compare.apply(self, args).then(function(result) {
var message;
if (self.isNot) {
result.pass = !result.pass;
}
args[0] = promiseForMessage;
message = j$.Expectation.finalizeMessage(self.util, name, self.isNot, args, result);
self.addExpectationResult(result.pass, {
matcherName: name,
passed: result.pass,
message: message,
error: undefined,
errorForStack: errorForStack,
actual: self.actual
});
});
};
}
/**
* Expect a promise to be resolved.
* @function
* @async
* @name async-matchers#toBeResolved
* @example
* await expectAsync(aPromise).toBeResolved();
* @example
* return expectAsync(aPromise).toBeResolved();
*/
AsyncExpectation.prototype.toBeResolved = function(actual) {
return actual.then(
function() { return {pass: true}; },
function() { return {pass: false}; }
);
};
/**
* Expect a promise to be rejected.
* @function
* @async
* @name async-matchers#toBeRejected
* @example
* await expectAsync(aPromise).toBeRejected();
* @example
* return expectAsync(aPromise).toBeRejected();
*/
AsyncExpectation.prototype.toBeRejected = function(actual) {
return actual.then(
function() { return {pass: false}; },
function() { return {pass: true}; }
);
};
/**
* Expect a promise to be resolved to a value equal to the expected, using deep equality comparison.
* @function
* @async
* @name async-matchers#toBeResolvedTo
* @param {Object} expected - Value that the promise is expected to resolve to
* @example
* await expectAsync(aPromise).toBeResolvedTo({prop: 'value'});
* @example
* return expectAsync(aPromise).toBeResolvedTo({prop: 'value'});
*/
AsyncExpectation.prototype.toBeResolvedTo = function(actualPromise, expectedValue) {
var self = this;
function prefix(passed) {
return 'Expected a promise ' +
(passed ? 'not ' : '') +
'to be resolved to ' + j$.pp(expectedValue);
}
return actualPromise.then(
function(actualValue) {
if (self.util.equals(actualValue, expectedValue, self.customEqualityTesters)) {
return {
pass: true,
message: prefix(true) + '.'
};
} else {
return {
pass: false,
message: prefix(false) + ' but it was resolved to ' + j$.pp(actualValue) + '.'
};
}
},
function() {
return {
pass: false,
message: prefix(false) + ' but it was rejected.'
};
}
);
};
AsyncExpectation.factory = function(options) {
var expect = new AsyncExpectation(options);
options = j$.util.clone(options);
options.isNot = true;
expect.not = new AsyncExpectation(options);
return expect;
};
return AsyncExpectation;
};
getJasmineRequireObj().CallTracker = function(j$) {
/**
@@ -2608,7 +2792,7 @@ getJasmineRequireObj().Expectation = function() {
return function() {
var args = Array.prototype.slice.call(arguments, 0),
expected = args.slice(0),
message = '';
message;
args.unshift(this.actual);
@@ -2626,20 +2810,7 @@ getJasmineRequireObj().Expectation = function() {
}
var result = matcherCompare.apply(null, args);
if (!result.pass) {
if (!result.message) {
args.unshift(this.isNot);
args.unshift(name);
message = this.util.buildFailureMessage.apply(null, args);
} else {
if (Object.prototype.toString.apply(result.message) === '[object Function]') {
message = result.message();
} else {
message = result.message;
}
}
}
message = Expectation.finalizeMessage(this.util, name, this.isNot, args, result);
if (expected.length == 1) {
expected = expected[0];
@@ -2660,6 +2831,23 @@ getJasmineRequireObj().Expectation = function() {
};
};
Expectation.finalizeMessage = function(util, name, isNot, args, result) {
if (result.pass) {
return '';
} else if (result.message) {
if (Object.prototype.toString.apply(result.message) === '[object Function]') {
return result.message();
} else {
return result.message;
}
} else {
args = args.slice();
args.unshift(isNot);
args.unshift(name);
return util.buildFailureMessage.apply(null, args);
}
};
Expectation.addCoreMatchers = function(matchers) {
var prototype = Expectation.prototype;
for (var matcherName in matchers) {
@@ -2731,7 +2919,9 @@ getJasmineRequireObj().buildExpectationResult = function() {
var error = options.error;
if (!error) {
if (options.stack) {
if (options.errorForStack) {
error = options.errorForStack;
} else if (options.stack) {
error = options;
} else {
try {
@@ -5177,6 +5367,25 @@ getJasmineRequireObj().interface = function(jasmine, env) {
return env.expect(actual);
},
/**
* Create an asynchronous expectation for a spec. Note that the matchers
* that are provided by an asynchronous expectation all return promises
* which must be either returned from the spec or waited for using `await`
* in order for Jasmine to associate them with the correct spec.
* @name expectAsync
* @function
* @global
* @param {Object} actual - Actual computed value to test expectations against.
* @return {async-matchers}
* @example
* await expectAsync(somePromise).toBeResolved();
* @example
* return expectAsync(somePromise).toBeResolved();
*/
expectAsync: function(actual) {
return env.expectAsync(actual);
},
/**
* Mark a spec as pending, expectation results will be ignored.
* @name pending
@@ -5904,6 +6113,7 @@ getJasmineRequireObj().Suite = function(j$) {
this.parentSuite = attrs.parentSuite;
this.description = attrs.description;
this.expectationFactory = attrs.expectationFactory;
this.asyncExpectationFactory = attrs.asyncExpectationFactory;
this.expectationResultFactory = attrs.expectationResultFactory;
this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure;
@@ -5936,6 +6146,10 @@ getJasmineRequireObj().Suite = function(j$) {
return this.expectationFactory(actual, this);
};
Suite.prototype.expectAsync = function(actual) {
return this.asyncExpectationFactory(actual, this);
};
Suite.prototype.getFullName = function() {
var fullName = [];
for (var parentSuite = this; parentSuite; parentSuite = parentSuite.parentSuite) {

View File

@@ -0,0 +1,288 @@
describe('AsyncExpectation', function() {
describe('Factory', function() {
it('throws an Error if promises are not available', function() {
var thenable = {then: function() {}},
options = {global: {}, actual: thenable}
function f() { jasmineUnderTest.AsyncExpectation.factory(options); }
expect(f).toThrowError('expectAsync is unavailable because the environment does not support promises.');
});
it('throws an Error if the argument is not a promise', function() {
jasmine.getEnv().requirePromises();
function f() {
jasmineUnderTest.AsyncExpectation.factory({actual: 'not a promise'});
}
expect(f).toThrowError('Expected expectAsync to be called with a promise.');
});
});
describe('#toBeResolved', function() {
it('passes if the actual is resolved', function() {
jasmine.getEnv().requirePromises();
var addExpectationResult = jasmine.createSpy('addExpectationResult'),
actual = Promise.resolve(),
expectation = new jasmineUnderTest.AsyncExpectation({
util: jasmineUnderTest.matchersUtil,
actual: actual,
addExpectationResult: addExpectationResult
});
return expectation.toBeResolved().then(function() {
expect(addExpectationResult).toHaveBeenCalledWith(true, {
matcherName: 'toBeResolved',
passed: true,
message: '',
error: undefined,
errorForStack: jasmine.any(Error),
actual: actual
});
});
});
it('fails if the actual is rejected', function() {
jasmine.getEnv().requirePromises();
var addExpectationResult = jasmine.createSpy('addExpectationResult'),
actual = Promise.reject('AsyncExpectationSpec rejection'),
expectation = new jasmineUnderTest.AsyncExpectation({
util: jasmineUnderTest.matchersUtil,
actual: actual,
addExpectationResult: addExpectationResult
});
return expectation.toBeResolved().then(function() {
expect(addExpectationResult).toHaveBeenCalledWith(false, {
matcherName: 'toBeResolved',
passed: false,
message: 'Expected a promise to be resolved.',
error: undefined,
errorForStack: jasmine.any(Error),
actual: actual
});
});
});
});
describe('#toBeRejected', function() {
it('passes if the actual is rejected', function() {
jasmine.getEnv().requirePromises();
var addExpectationResult = jasmine.createSpy('addExpectationResult'),
actual = Promise.reject('AsyncExpectationSpec rejection'),
expectation = new jasmineUnderTest.AsyncExpectation({
util: jasmineUnderTest.matchersUtil,
actual: actual,
addExpectationResult: addExpectationResult
});
return expectation.toBeRejected().then(function() {
expect(addExpectationResult).toHaveBeenCalledWith(true, {
matcherName: 'toBeRejected',
passed: true,
message: '',
error: undefined,
errorForStack: jasmine.any(Error),
actual: actual
});
});
});
it('fails if the actual is resolved', function() {
jasmine.getEnv().requirePromises();
var addExpectationResult = jasmine.createSpy('addExpectationResult'),
actual = Promise.resolve(),
expectation = new jasmineUnderTest.AsyncExpectation({
util: jasmineUnderTest.matchersUtil,
actual: actual,
addExpectationResult: addExpectationResult
});
return expectation.toBeRejected().then(function() {
expect(addExpectationResult).toHaveBeenCalledWith(false, {
matcherName: 'toBeRejected',
passed: false,
message: 'Expected a promise to be rejected.',
error: undefined,
errorForStack: jasmine.any(Error),
actual: actual
});
});
});
});
describe('#toBeResolvedTo', function() {
it('passes if the promise is resolved to the expected value', function() {
jasmine.getEnv().requirePromises();
var actual = Promise.resolve({foo: 42});
var addExpectationResult = jasmine.createSpy('addExpectationResult'),
expectation = new jasmineUnderTest.AsyncExpectation({
util: jasmineUnderTest.matchersUtil,
actual: actual,
addExpectationResult: addExpectationResult
});
return expectation.toBeResolvedTo({foo: 42}).then(function() {
expect(addExpectationResult).toHaveBeenCalledWith(true, {
matcherName: 'toBeResolvedTo',
passed: true,
message: '',
error: undefined,
errorForStack: jasmine.any(Error),
actual: actual
});
});
});
it('fails if the promise is rejected', function() {
jasmine.getEnv().requirePromises();
var actual = Promise.reject('AsyncExpectationSpec error');
var addExpectationResult = jasmine.createSpy('addExpectationResult'),
expectation = new jasmineUnderTest.AsyncExpectation({
util: jasmineUnderTest.matchersUtil,
actual: actual,
addExpectationResult: addExpectationResult
});
return expectation.toBeResolvedTo('').then(function() {
expect(addExpectationResult).toHaveBeenCalledWith(false, {
matcherName: 'toBeResolvedTo',
passed: false,
message: "Expected a promise to be resolved to '' but it was rejected.",
error: undefined,
errorForStack: jasmine.any(Error),
actual: actual
});
});
});
it('fails if the promise is resolved to a different value', function() {
jasmine.getEnv().requirePromises();
var actual = Promise.resolve({foo: 17});
var addExpectationResult = jasmine.createSpy('addExpectationResult'),
expectation = new jasmineUnderTest.AsyncExpectation({
util: jasmineUnderTest.matchersUtil,
actual: actual,
addExpectationResult: addExpectationResult
});
return expectation.toBeResolvedTo({foo: 42}).then(function() {
expect(addExpectationResult).toHaveBeenCalledWith(false, {
matcherName: 'toBeResolvedTo',
passed: false,
message: 'Expected a promise to be resolved to Object({ foo: 42 }) but it was resolved to Object({ foo: 17 }).',
error: undefined,
errorForStack: jasmine.any(Error),
actual: actual
});
});
});
it('builds its message correctly when negated', function() {
jasmine.getEnv().requirePromises();
var addExpectationResult = jasmine.createSpy('addExpectationResult'),
expectation = jasmineUnderTest.AsyncExpectation.factory({
util: jasmineUnderTest.matchersUtil,
actual: Promise.resolve(true),
addExpectationResult: addExpectationResult
});
return expectation.not.toBeResolvedTo(true).then(function() {
expect(addExpectationResult).toHaveBeenCalledWith(false,
jasmine.objectContaining({
passed: false,
message: 'Expected a promise not to be resolved to true.'
})
);
});
});
it('supports custom equality testers', function() {
jasmine.getEnv().requirePromises();
var addExpectationResult = jasmine.createSpy('addExpectationResult'),
expectation = new jasmineUnderTest.AsyncExpectation({
util: jasmineUnderTest.matchersUtil,
customEqualityTesters: [function() { return true; }],
actual: Promise.resolve('actual'),
addExpectationResult: addExpectationResult
});
return expectation.toBeResolvedTo('expected').then(function() {
expect(addExpectationResult).toHaveBeenCalledWith(true,
jasmine.objectContaining({passed: true}));
});
});
});
describe('#not', function() {
it('converts a pass to a fail', function() {
jasmine.getEnv().requirePromises();
var addExpectationResult = jasmine.createSpy('addExpectationResult'),
actual = Promise.resolve(),
expectation = jasmineUnderTest.AsyncExpectation.factory({
util: jasmineUnderTest.matchersUtil,
actual: actual,
addExpectationResult: addExpectationResult
});
return expectation.not.toBeResolved().then(function() {
expect(addExpectationResult).toHaveBeenCalledWith(false,
jasmine.objectContaining({
passed: false,
message: 'Expected a promise not to be resolved.'
})
);
});
});
it('converts a fail to a pass', function() {
jasmine.getEnv().requirePromises();
var addExpectationResult = jasmine.createSpy('addExpectationResult'),
actual = Promise.reject(),
expectation = jasmineUnderTest.AsyncExpectation.factory({
util: jasmineUnderTest.matchersUtil,
actual: actual,
addExpectationResult: addExpectationResult
});
return expectation.not.toBeResolved().then(function() {
expect(addExpectationResult).toHaveBeenCalledWith(true,
jasmine.objectContaining({
passed: true,
message: ''
})
);
});
});
});
it('propagates rejections from the comparison function', function() {
jasmine.getEnv().requirePromises();
var error = new Error('AsyncExpectationSpec failure');
spyOn(jasmineUnderTest.AsyncExpectation.prototype, 'toBeResolved')
.and.returnValue(Promise.reject(error));
var addExpectationResult = jasmine.createSpy('addExpectationResult'),
actual = dummyPromise(),
expectation = new jasmineUnderTest.AsyncExpectation({
actual: actual,
addExpectationResult: addExpectationResult
});
return expectation.toBeResolved()
.then(
function() { fail('Expected a rejection'); },
function(e) { expect(e).toBe(error); }
);
});
function dummyPromise() {
return new Promise(function(resolve, reject) {
});
}
});

View File

@@ -44,6 +44,21 @@ describe("buildExpectationResult", function() {
expect(result.stack).toEqual('foo');
});
it("delegates stack formatting to the provided formatter if there was a provided errorForStack", function() {
var fakeError = {stack: 'foo'},
stackFormatter = jasmine.createSpy("stack formatter").and.returnValue(fakeError.stack);
var result = jasmineUnderTest.buildExpectationResult(
{
passed: false,
errorForStack: fakeError,
stackFormatter: stackFormatter
});
expect(stackFormatter).toHaveBeenCalledWith(fakeError);
expect(result.stack).toEqual('foo');
});
it("matcherName returns passed matcherName", function() {
var result = jasmineUnderTest.buildExpectationResult({matcherName: 'some-value'});
expect(result.matcherName).toBe('some-value');

View File

@@ -2415,4 +2415,106 @@ describe("Env integration", function() {
env.execute();
});
it('supports async matchers', function(done) {
jasmine.getEnv().requirePromises();
var env = new jasmineUnderTest.Env(),
specDone = jasmine.createSpy('specDone'),
suiteDone = jasmine.createSpy('suiteDone');
env.addReporter({
specDone: specDone,
suiteDone: suiteDone,
jasmineDone: function(result) {
expect(result.failedExpectations).toEqual([jasmine.objectContaining({
message: 'Expected a promise to be rejected.'
})]);
expect(specDone).toHaveBeenCalledWith(jasmine.objectContaining({
description: 'has an async failure',
failedExpectations: [jasmine.objectContaining({
message: 'Expected a promise to be rejected.'
})]
}));
expect(suiteDone).toHaveBeenCalledWith(jasmine.objectContaining({
description: 'a suite',
failedExpectations: [jasmine.objectContaining({
message: 'Expected a promise to be rejected.'
})]
}));
done();
}
});
function fail(innerDone) {
var resolve;
var p = new Promise(function(res, rej) { resolve = res });
env.expectAsync(p).toBeRejected().then(innerDone);
resolve();
}
env.afterAll(fail);
env.describe('a suite', function() {
env.afterAll(fail);
env.it('has an async failure', fail);
});
env.execute();
});
it('provides custom equality testers to async matchers', function(done) {
jasmine.getEnv().requirePromises();
var env = new jasmineUnderTest.Env(),
specDone = jasmine.createSpy('specDone');
env.addReporter({
specDone: specDone,
jasmineDone: function() {
expect(specDone).toHaveBeenCalledWith(jasmine.objectContaining({
description: 'has an async failure',
failedExpectations: []
}));
done();
}
});
env.it('has an async failure', function() {
env.addCustomEqualityTester(function() { return true; });
var p = Promise.resolve('something');
return env.expectAsync(p).toBeResolvedTo('something else');
});
env.execute();
});
it('includes useful stack frames in async matcher failures', function(done) {
jasmine.getEnv().requirePromises();
var env = new jasmineUnderTest.Env(),
specDone = jasmine.createSpy('specDone');
env.addReporter({
specDone: specDone,
jasmineDone: function() {
expect(specDone).toHaveBeenCalledWith(jasmine.objectContaining({
failedExpectations: [jasmine.objectContaining({
stack: jasmine.stringMatching('EnvSpec.js')
})]
}));
done();
}
});
env.it('has an async failure', function() {
env.addCustomEqualityTester(function() { return true; });
var p = Promise.resolve();
return env.expectAsync(p).toBeRejected();
});
env.execute();
});
});

8
spec/helpers/promises.js Normal file
View File

@@ -0,0 +1,8 @@
(function(env) {
env.requirePromises = function() {
if (typeof Promise !== 'function') {
env.pending("Environment does not support promises");
}
};
})(jasmine.getEnv());

View File

@@ -11,6 +11,7 @@
"helpers/checkForSymbol.js",
"helpers/checkForTypedArrays.js",
"helpers/integrationMatchers.js",
"helpers/promises.js",
"helpers/nodeDefineJasmineUnderTest.js"
],
"random": true

View File

@@ -24,6 +24,7 @@ helpers:
- 'helpers/checkForSymbol.js'
- 'helpers/checkForTypedArrays.js'
- 'helpers/integrationMatchers.js'
- 'helpers/promises.js'
- 'helpers/defineJasmineUnderTest.js'
spec_files:
- '**/*[Ss]pec.js'

View File

@@ -0,0 +1,153 @@
getJasmineRequireObj().AsyncExpectation = function(j$) {
var promiseForMessage = {
jasmineToString: function() { return 'a promise'; }
};
/**
* Asynchronous matchers.
* @namespace async-matchers
*/
function AsyncExpectation(options) {
var global = options.global || j$.getGlobal();
this.util = options.util || { buildFailureMessage: function() {} };
this.customEqualityTesters = options.customEqualityTesters || [];
this.addExpectationResult = options.addExpectationResult || function(){};
this.actual = options.actual;
this.isNot = options.isNot;
if (!global.Promise) {
throw new Error('expectAsync is unavailable because the environment does not support promises.');
}
if (!j$.isPromise(this.actual)) {
throw new Error('Expected expectAsync to be called with a promise.');
}
['toBeResolved', 'toBeRejected', 'toBeResolvedTo'].forEach(wrapCompare.bind(this));
}
function wrapCompare(name) {
var compare = this[name];
this[name] = function() {
var self = this;
var args = Array.prototype.slice.call(arguments);
args.unshift(this.actual);
// Capture the call stack here, before we go async, so that it will
// contain frames that are relevant to the user instead of just parts
// of Jasmine.
var errorForStack = j$.util.errorWithStack();
return compare.apply(self, args).then(function(result) {
var message;
if (self.isNot) {
result.pass = !result.pass;
}
args[0] = promiseForMessage;
message = j$.Expectation.finalizeMessage(self.util, name, self.isNot, args, result);
self.addExpectationResult(result.pass, {
matcherName: name,
passed: result.pass,
message: message,
error: undefined,
errorForStack: errorForStack,
actual: self.actual
});
});
};
}
/**
* Expect a promise to be resolved.
* @function
* @async
* @name async-matchers#toBeResolved
* @example
* await expectAsync(aPromise).toBeResolved();
* @example
* return expectAsync(aPromise).toBeResolved();
*/
AsyncExpectation.prototype.toBeResolved = function(actual) {
return actual.then(
function() { return {pass: true}; },
function() { return {pass: false}; }
);
};
/**
* Expect a promise to be rejected.
* @function
* @async
* @name async-matchers#toBeRejected
* @example
* await expectAsync(aPromise).toBeRejected();
* @example
* return expectAsync(aPromise).toBeRejected();
*/
AsyncExpectation.prototype.toBeRejected = function(actual) {
return actual.then(
function() { return {pass: false}; },
function() { return {pass: true}; }
);
};
/**
* Expect a promise to be resolved to a value equal to the expected, using deep equality comparison.
* @function
* @async
* @name async-matchers#toBeResolvedTo
* @param {Object} expected - Value that the promise is expected to resolve to
* @example
* await expectAsync(aPromise).toBeResolvedTo({prop: 'value'});
* @example
* return expectAsync(aPromise).toBeResolvedTo({prop: 'value'});
*/
AsyncExpectation.prototype.toBeResolvedTo = function(actualPromise, expectedValue) {
var self = this;
function prefix(passed) {
return 'Expected a promise ' +
(passed ? 'not ' : '') +
'to be resolved to ' + j$.pp(expectedValue);
}
return actualPromise.then(
function(actualValue) {
if (self.util.equals(actualValue, expectedValue, self.customEqualityTesters)) {
return {
pass: true,
message: prefix(true) + '.'
};
} else {
return {
pass: false,
message: prefix(false) + ' but it was resolved to ' + j$.pp(actualValue) + '.'
};
}
},
function() {
return {
pass: false,
message: prefix(false) + ' but it was rejected.'
};
}
);
};
AsyncExpectation.factory = function(options) {
var expect = new AsyncExpectation(options);
options = j$.util.clone(options);
options.isNot = true;
expect.not = new AsyncExpectation(options);
return expect;
};
return AsyncExpectation;
};

View File

@@ -117,6 +117,19 @@ getJasmineRequireObj().Env = function(j$) {
}
};
var asyncExpectationFactory = function(actual, spec) {
return j$.AsyncExpectation.factory({
util: j$.matchersUtil,
customEqualityTesters: runnableResources[spec.id].customEqualityTesters,
actual: actual,
addExpectationResult: addExpectationResult
});
function addExpectationResult(passed, result) {
return spec.addExpectationResult(passed, result);
}
};
var defaultResourcesForRunnable = function(id, parentRunnableId) {
var resources = {spies: [], customEqualityTesters: [], customMatchers: {}, customSpyStrategies: {}};
@@ -247,6 +260,7 @@ getJasmineRequireObj().Env = function(j$) {
id: getNextSuiteId(),
description: 'Jasmine__TopLevel__Suite',
expectationFactory: expectationFactory,
asyncExpectationFactory: asyncExpectationFactory,
expectationResultFactory: expectationResultFactory
});
defaultResourcesForRunnable(topSuite.id);
@@ -521,6 +535,7 @@ getJasmineRequireObj().Env = function(j$) {
description: description,
parentSuite: currentDeclarationSuite,
expectationFactory: expectationFactory,
asyncExpectationFactory: asyncExpectationFactory,
expectationResultFactory: expectationResultFactory,
throwOnExpectationFailure: throwOnExpectationFailure
});
@@ -614,6 +629,7 @@ getJasmineRequireObj().Env = function(j$) {
id: getNextSpecId(),
beforeAndAfterFns: beforeAndAfterFns(suite),
expectationFactory: expectationFactory,
asyncExpectationFactory: asyncExpectationFactory,
resultCallback: specResultCallback,
getSpecName: function(spec) {
return getSpecName(spec, suite);
@@ -695,6 +711,14 @@ getJasmineRequireObj().Env = function(j$) {
return currentRunnable().expect(actual);
};
this.expectAsync = function(actual) {
if (!currentRunnable()) {
throw new Error('\'expectAsync\' was used when there was no current spec, this could be because an asynchronous test timed out');
}
return currentRunnable().expectAsync(actual);
};
this.beforeEach = function(beforeEachFunction, timeout) {
ensureIsNotNested('beforeEach');
ensureIsFunctionOrAsync(beforeEachFunction, 'beforeEach');

View File

@@ -21,7 +21,7 @@ getJasmineRequireObj().Expectation = function() {
return function() {
var args = Array.prototype.slice.call(arguments, 0),
expected = args.slice(0),
message = '';
message;
args.unshift(this.actual);
@@ -39,20 +39,7 @@ getJasmineRequireObj().Expectation = function() {
}
var result = matcherCompare.apply(null, args);
if (!result.pass) {
if (!result.message) {
args.unshift(this.isNot);
args.unshift(name);
message = this.util.buildFailureMessage.apply(null, args);
} else {
if (Object.prototype.toString.apply(result.message) === '[object Function]') {
message = result.message();
} else {
message = result.message;
}
}
}
message = Expectation.finalizeMessage(this.util, name, this.isNot, args, result);
if (expected.length == 1) {
expected = expected[0];
@@ -73,6 +60,23 @@ getJasmineRequireObj().Expectation = function() {
};
};
Expectation.finalizeMessage = function(util, name, isNot, args, result) {
if (result.pass) {
return '';
} else if (result.message) {
if (Object.prototype.toString.apply(result.message) === '[object Function]') {
return result.message();
} else {
return result.message;
}
} else {
args = args.slice();
args.unshift(isNot);
args.unshift(name);
return util.buildFailureMessage.apply(null, args);
}
};
Expectation.addCoreMatchers = function(matchers) {
var prototype = Expectation.prototype;
for (var matcherName in matchers) {

View File

@@ -45,7 +45,9 @@ getJasmineRequireObj().buildExpectationResult = function() {
var error = options.error;
if (!error) {
if (options.stack) {
if (options.errorForStack) {
error = options.errorForStack;
} else if (options.stack) {
error = options;
} else {
try {

View File

@@ -1,6 +1,7 @@
getJasmineRequireObj().Spec = function(j$) {
function Spec(attrs) {
this.expectationFactory = attrs.expectationFactory;
this.asyncExpectationFactory = attrs.asyncExpectationFactory;
this.resultCallback = attrs.resultCallback || function() {};
this.id = attrs.id;
this.description = attrs.description || '';
@@ -57,6 +58,10 @@ getJasmineRequireObj().Spec = function(j$) {
return this.expectationFactory(actual, this);
};
Spec.prototype.expectAsync = function(actual) {
return this.asyncExpectationFactory(actual, this);
};
Spec.prototype.execute = function(onComplete, excluded) {
var self = this;

View File

@@ -5,6 +5,7 @@ getJasmineRequireObj().Suite = function(j$) {
this.parentSuite = attrs.parentSuite;
this.description = attrs.description;
this.expectationFactory = attrs.expectationFactory;
this.asyncExpectationFactory = attrs.asyncExpectationFactory;
this.expectationResultFactory = attrs.expectationResultFactory;
this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure;
@@ -37,6 +38,10 @@ getJasmineRequireObj().Suite = function(j$) {
return this.expectationFactory(actual, this);
};
Suite.prototype.expectAsync = function(actual) {
return this.asyncExpectationFactory(actual, this);
};
Suite.prototype.getFullName = function() {
var fullName = [];
for (var parentSuite = this; parentSuite; parentSuite = parentSuite.parentSuite) {

View File

@@ -120,7 +120,7 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) {
};
j$.isPromise = function(obj) {
return typeof jasmineGlobal.Promise !== 'undefined' && obj.constructor === jasmineGlobal.Promise;
return typeof jasmineGlobal.Promise !== 'undefined' && obj && obj.constructor === jasmineGlobal.Promise;
};
j$.fnNameFor = function(func) {

View File

@@ -38,6 +38,7 @@ var getJasmineRequireObj = (function (jasmineGlobal) {
j$.StackTrace = jRequire.StackTrace(j$);
j$.ExceptionFormatter = jRequire.ExceptionFormatter(j$);
j$.Expectation = jRequire.Expectation();
j$.AsyncExpectation = jRequire.AsyncExpectation(j$);
j$.buildExpectationResult = jRequire.buildExpectationResult();
j$.JsApiReporter = jRequire.JsApiReporter();
j$.matchersUtil = jRequire.matchersUtil(j$);

View File

@@ -167,6 +167,25 @@ getJasmineRequireObj().interface = function(jasmine, env) {
return env.expect(actual);
},
/**
* Create an asynchronous expectation for a spec. Note that the matchers
* that are provided by an asynchronous expectation all return promises
* which must be either returned from the spec or waited for using `await`
* in order for Jasmine to associate them with the correct spec.
* @name expectAsync
* @function
* @global
* @param {Object} actual - Actual computed value to test expectations against.
* @return {async-matchers}
* @example
* await expectAsync(somePromise).toBeResolved();
* @example
* return expectAsync(somePromise).toBeResolved();
*/
expectAsync: function(actual) {
return env.expectAsync(actual);
},
/**
* Mark a spec as pending, expectation results will be ignored.
* @name pending