Added throwUnless and throwUnlessAsync
These are similar to `expect` and `expectAsync` except that they throw exceptions rather than recording matcher failures as spec/suite failures. They're intended to support using Jasmine matchers in testing-library's `waitFor`, and also provide a way to integration-test custom matchers. These funtions are not equivalent to `expect` and `expectAsync` and should not be used in situations where you want a matcher failure to reliably fail the spec. Whether that happens depends on the structure of the surrounding code. In general, you should only use `throwUnless` when you expect something (which could be your own code or library code like `waitFor`) to catch the resulting exception. Fixes #2003. Fixes #1980.
This commit is contained in:
@@ -1393,6 +1393,49 @@ getJasmineRequireObj().Env = function(j$) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleThrowUnlessFailure = function(passed, result) {
|
||||||
|
if (!passed) {
|
||||||
|
/**
|
||||||
|
* @interface
|
||||||
|
* @name ThrowUnlessFailure
|
||||||
|
* @extends Error
|
||||||
|
* @description Represents a failure of an expectation evaluated with
|
||||||
|
* {@link throwUnless}. Properties of this error are a subset of the
|
||||||
|
* properties of {@link Expectation} and have the same values.
|
||||||
|
* @property {String} matcherName - The name of the matcher that was executed for this expectation.
|
||||||
|
* @property {String} message - The failure message for the expectation.
|
||||||
|
* @property {Boolean} passed - Whether the expectation passed or failed.
|
||||||
|
* @property {Object} expected - If the expectation failed, what was the expected value.
|
||||||
|
* @property {Object} actual - If the expectation failed, what actual value was produced.
|
||||||
|
*/
|
||||||
|
const error = new Error(result.message);
|
||||||
|
error.passed = result.passed;
|
||||||
|
error.message = result.message;
|
||||||
|
error.expected = result.expected;
|
||||||
|
error.actual = result.actual;
|
||||||
|
error.matcherName = result.matcherName;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const throwUnlessFactory = function(actual, spec) {
|
||||||
|
return j$.Expectation.factory({
|
||||||
|
matchersUtil: runableResources.makeMatchersUtil(),
|
||||||
|
customMatchers: runableResources.customMatchers(),
|
||||||
|
actual: actual,
|
||||||
|
addExpectationResult: handleThrowUnlessFailure
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const throwUnlessAsyncFactory = function(actual, spec) {
|
||||||
|
return j$.Expectation.asyncFactory({
|
||||||
|
matchersUtil: runableResources.makeMatchersUtil(),
|
||||||
|
customAsyncMatchers: runableResources.customAsyncMatchers(),
|
||||||
|
actual: actual,
|
||||||
|
addExpectationResult: handleThrowUnlessFailure
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: Unify recordLateError with recordLateExpectation? The extra
|
// TODO: Unify recordLateError with recordLateExpectation? The extra
|
||||||
// diagnostic info added by the latter is probably useful in most cases.
|
// diagnostic info added by the latter is probably useful in most cases.
|
||||||
function recordLateError(error) {
|
function recordLateError(error) {
|
||||||
@@ -1919,6 +1962,16 @@ getJasmineRequireObj().Env = function(j$) {
|
|||||||
return runable.asyncExpectationFactory(actual, runable);
|
return runable.asyncExpectationFactory(actual, runable);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.throwUnless = function(actual) {
|
||||||
|
const runable = runner.currentRunable();
|
||||||
|
return throwUnlessFactory(actual, runable);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.throwUnlessAsync = function(actual) {
|
||||||
|
const runable = runner.currentRunable();
|
||||||
|
return throwUnlessAsyncFactory(actual, runable);
|
||||||
|
};
|
||||||
|
|
||||||
this.beforeEach = function(beforeEachFunction, timeout) {
|
this.beforeEach = function(beforeEachFunction, timeout) {
|
||||||
ensureIsNotNested('beforeEach');
|
ensureIsNotNested('beforeEach');
|
||||||
ensureNonParallelOrInHelperOrInDescribe('beforeEach');
|
ensureNonParallelOrInHelperOrInDescribe('beforeEach');
|
||||||
@@ -8228,6 +8281,50 @@ getJasmineRequireObj().interface = function(jasmine, env) {
|
|||||||
return env.expectAsync(actual);
|
return env.expectAsync(actual);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an asynchronous expectation for a spec and throw an error if it fails.
|
||||||
|
*
|
||||||
|
* This is intended to allow Jasmine matchers to be used with tools like
|
||||||
|
* testing-library's `waitFor`, which expect matcher failures to throw
|
||||||
|
* exceptions and not trigger a spec failure if the exception is caught.
|
||||||
|
* It can also be used to integration-test custom matchers.
|
||||||
|
*
|
||||||
|
* If the resulting expectation fails, a {@link ThrowUnlessFailure} will be
|
||||||
|
* thrown. A failed expectation will not result in a spec failure unless the
|
||||||
|
* exception propagates back to Jasmine, either via the call stack or via
|
||||||
|
* the global unhandled exception/unhandled promise rejection events.
|
||||||
|
* @name throwUnlessAsync
|
||||||
|
* @param actual
|
||||||
|
* @global
|
||||||
|
* @param {Object} actual - Actual computed value to test expectations against.
|
||||||
|
* @return {matchers}
|
||||||
|
*/
|
||||||
|
throwUnlessAsync: function(actual) {
|
||||||
|
return env.throwUnless(actual);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an expectation for a spec and throw an error if it fails.
|
||||||
|
*
|
||||||
|
* This is intended to allow Jasmine matchers to be used with tools like
|
||||||
|
* testing-library's `waitFor`, which expect matcher failures to throw
|
||||||
|
* exceptions and not trigger a spec failure if the exception is caught.
|
||||||
|
* It can also be used to integration-test custom matchers.
|
||||||
|
*
|
||||||
|
* If the resulting expectation fails, a {@link ThrowUnlessFailure} will be
|
||||||
|
* thrown. A failed expectation will not result in a spec failure unless the
|
||||||
|
* exception propagates back to Jasmine, either via the call stack or via
|
||||||
|
* the global unhandled exception/unhandled promise rejection events.
|
||||||
|
* @name throwUnless
|
||||||
|
* @param actual
|
||||||
|
* @global
|
||||||
|
* @param {Object} actual - Actual computed value to test expectations against.
|
||||||
|
* @return {matchers}
|
||||||
|
*/
|
||||||
|
throwUnless: function(actual) {
|
||||||
|
return env.throwUnless(actual);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark a spec as pending, expectation results will be ignored.
|
* Mark a spec as pending, expectation results will be ignored.
|
||||||
* @name pending
|
* @name pending
|
||||||
|
|||||||
+2
-1
@@ -97,7 +97,8 @@
|
|||||||
],
|
],
|
||||||
"space-before-blocks": "error",
|
"space-before-blocks": "error",
|
||||||
"no-eval": "error",
|
"no-eval": "error",
|
||||||
"no-var": "error"
|
"no-var": "error",
|
||||||
|
"no-debugger": "error"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
|
|||||||
@@ -4334,6 +4334,115 @@ describe('Env integration', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('throwUnless', function() {
|
||||||
|
it('throws when the matcher fails', async function() {
|
||||||
|
let thrown;
|
||||||
|
|
||||||
|
env.it('a spec', function() {
|
||||||
|
try {
|
||||||
|
env.throwUnless(1).toEqual(2);
|
||||||
|
} catch (e) {
|
||||||
|
thrown = e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await env.execute();
|
||||||
|
expect(thrown).toBeInstanceOf(Error);
|
||||||
|
expect(thrown.passed).toEqual(false);
|
||||||
|
expect(thrown.matcherName).toEqual('toEqual');
|
||||||
|
expect(thrown.message).toEqual('Expected 1 to equal 2.');
|
||||||
|
expect(thrown.actual).toEqual(1);
|
||||||
|
expect(thrown.expected).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not throw when the matcher passes', async function() {
|
||||||
|
let threw = false;
|
||||||
|
|
||||||
|
env.it('a spec', function() {
|
||||||
|
try {
|
||||||
|
env.throwUnless(1).toEqual(1);
|
||||||
|
} catch (e) {
|
||||||
|
threw = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await env.execute();
|
||||||
|
expect(threw).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not cause a failure if the error does not propagate back to jasmine', async function() {
|
||||||
|
env.it('a spec', function() {
|
||||||
|
try {
|
||||||
|
env.throwUnless(1).toEqual(2);
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const reporter = jasmine.createSpyObj('reporter', ['specDone']);
|
||||||
|
env.addReporter(reporter);
|
||||||
|
|
||||||
|
await env.execute();
|
||||||
|
expect(reporter.specDone).toHaveBeenCalledWith(
|
||||||
|
jasmine.objectContaining({ status: 'passed' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('throwUnlessAsync', function() {
|
||||||
|
it('throws when the matcher fails', async function() {
|
||||||
|
const promise = Promise.resolve('a');
|
||||||
|
let thrown;
|
||||||
|
|
||||||
|
env.it('a spec', async function() {
|
||||||
|
try {
|
||||||
|
await env.throwUnlessAsync(promise).toBeResolvedTo('b');
|
||||||
|
} catch (e) {
|
||||||
|
thrown = e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await env.execute();
|
||||||
|
expect(thrown).toBeInstanceOf(Error);
|
||||||
|
expect(thrown.passed).toEqual(false);
|
||||||
|
expect(thrown.matcherName).toEqual('toBeResolvedTo');
|
||||||
|
expect(thrown.message).toEqual(
|
||||||
|
"Expected a promise to be resolved to 'b' but it was resolved to 'a'."
|
||||||
|
);
|
||||||
|
expect(thrown.actual).toBe(promise);
|
||||||
|
expect(thrown.expected).toEqual('b');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not throw when the matcher passes', async function() {
|
||||||
|
let threw = false;
|
||||||
|
|
||||||
|
env.it('a spec', async function() {
|
||||||
|
try {
|
||||||
|
await env.throwUnlessAsync(Promise.resolve()).toBeResolved();
|
||||||
|
} catch (e) {
|
||||||
|
threw = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await env.execute();
|
||||||
|
expect(threw).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not cause a failure if the error does not propagate back to jasmine', async function() {
|
||||||
|
env.it('a spec', async function() {
|
||||||
|
try {
|
||||||
|
await env.throwUnlessAsync(Promise.resolve()).toBeRejected();
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const reporter = jasmine.createSpyObj('reporter', ['specDone']);
|
||||||
|
env.addReporter(reporter);
|
||||||
|
|
||||||
|
await env.execute();
|
||||||
|
expect(reporter.specDone).toHaveBeenCalledWith(
|
||||||
|
jasmine.objectContaining({ status: 'passed' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function browserEventMethods() {
|
function browserEventMethods() {
|
||||||
return {
|
return {
|
||||||
listeners_: { error: [], unhandledrejection: [] },
|
listeners_: { error: [], unhandledrejection: [] },
|
||||||
|
|||||||
@@ -264,6 +264,49 @@ getJasmineRequireObj().Env = function(j$) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleThrowUnlessFailure = function(passed, result) {
|
||||||
|
if (!passed) {
|
||||||
|
/**
|
||||||
|
* @interface
|
||||||
|
* @name ThrowUnlessFailure
|
||||||
|
* @extends Error
|
||||||
|
* @description Represents a failure of an expectation evaluated with
|
||||||
|
* {@link throwUnless}. Properties of this error are a subset of the
|
||||||
|
* properties of {@link Expectation} and have the same values.
|
||||||
|
* @property {String} matcherName - The name of the matcher that was executed for this expectation.
|
||||||
|
* @property {String} message - The failure message for the expectation.
|
||||||
|
* @property {Boolean} passed - Whether the expectation passed or failed.
|
||||||
|
* @property {Object} expected - If the expectation failed, what was the expected value.
|
||||||
|
* @property {Object} actual - If the expectation failed, what actual value was produced.
|
||||||
|
*/
|
||||||
|
const error = new Error(result.message);
|
||||||
|
error.passed = result.passed;
|
||||||
|
error.message = result.message;
|
||||||
|
error.expected = result.expected;
|
||||||
|
error.actual = result.actual;
|
||||||
|
error.matcherName = result.matcherName;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const throwUnlessFactory = function(actual, spec) {
|
||||||
|
return j$.Expectation.factory({
|
||||||
|
matchersUtil: runableResources.makeMatchersUtil(),
|
||||||
|
customMatchers: runableResources.customMatchers(),
|
||||||
|
actual: actual,
|
||||||
|
addExpectationResult: handleThrowUnlessFailure
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const throwUnlessAsyncFactory = function(actual, spec) {
|
||||||
|
return j$.Expectation.asyncFactory({
|
||||||
|
matchersUtil: runableResources.makeMatchersUtil(),
|
||||||
|
customAsyncMatchers: runableResources.customAsyncMatchers(),
|
||||||
|
actual: actual,
|
||||||
|
addExpectationResult: handleThrowUnlessFailure
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: Unify recordLateError with recordLateExpectation? The extra
|
// TODO: Unify recordLateError with recordLateExpectation? The extra
|
||||||
// diagnostic info added by the latter is probably useful in most cases.
|
// diagnostic info added by the latter is probably useful in most cases.
|
||||||
function recordLateError(error) {
|
function recordLateError(error) {
|
||||||
@@ -790,6 +833,16 @@ getJasmineRequireObj().Env = function(j$) {
|
|||||||
return runable.asyncExpectationFactory(actual, runable);
|
return runable.asyncExpectationFactory(actual, runable);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.throwUnless = function(actual) {
|
||||||
|
const runable = runner.currentRunable();
|
||||||
|
return throwUnlessFactory(actual, runable);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.throwUnlessAsync = function(actual) {
|
||||||
|
const runable = runner.currentRunable();
|
||||||
|
return throwUnlessAsyncFactory(actual, runable);
|
||||||
|
};
|
||||||
|
|
||||||
this.beforeEach = function(beforeEachFunction, timeout) {
|
this.beforeEach = function(beforeEachFunction, timeout) {
|
||||||
ensureIsNotNested('beforeEach');
|
ensureIsNotNested('beforeEach');
|
||||||
ensureNonParallelOrInHelperOrInDescribe('beforeEach');
|
ensureNonParallelOrInHelperOrInDescribe('beforeEach');
|
||||||
|
|||||||
@@ -225,6 +225,50 @@ getJasmineRequireObj().interface = function(jasmine, env) {
|
|||||||
return env.expectAsync(actual);
|
return env.expectAsync(actual);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an asynchronous expectation for a spec and throw an error if it fails.
|
||||||
|
*
|
||||||
|
* This is intended to allow Jasmine matchers to be used with tools like
|
||||||
|
* testing-library's `waitFor`, which expect matcher failures to throw
|
||||||
|
* exceptions and not trigger a spec failure if the exception is caught.
|
||||||
|
* It can also be used to integration-test custom matchers.
|
||||||
|
*
|
||||||
|
* If the resulting expectation fails, a {@link ThrowUnlessFailure} will be
|
||||||
|
* thrown. A failed expectation will not result in a spec failure unless the
|
||||||
|
* exception propagates back to Jasmine, either via the call stack or via
|
||||||
|
* the global unhandled exception/unhandled promise rejection events.
|
||||||
|
* @name throwUnlessAsync
|
||||||
|
* @param actual
|
||||||
|
* @global
|
||||||
|
* @param {Object} actual - Actual computed value to test expectations against.
|
||||||
|
* @return {matchers}
|
||||||
|
*/
|
||||||
|
throwUnlessAsync: function(actual) {
|
||||||
|
return env.throwUnless(actual);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an expectation for a spec and throw an error if it fails.
|
||||||
|
*
|
||||||
|
* This is intended to allow Jasmine matchers to be used with tools like
|
||||||
|
* testing-library's `waitFor`, which expect matcher failures to throw
|
||||||
|
* exceptions and not trigger a spec failure if the exception is caught.
|
||||||
|
* It can also be used to integration-test custom matchers.
|
||||||
|
*
|
||||||
|
* If the resulting expectation fails, a {@link ThrowUnlessFailure} will be
|
||||||
|
* thrown. A failed expectation will not result in a spec failure unless the
|
||||||
|
* exception propagates back to Jasmine, either via the call stack or via
|
||||||
|
* the global unhandled exception/unhandled promise rejection events.
|
||||||
|
* @name throwUnless
|
||||||
|
* @param actual
|
||||||
|
* @global
|
||||||
|
* @param {Object} actual - Actual computed value to test expectations against.
|
||||||
|
* @return {matchers}
|
||||||
|
*/
|
||||||
|
throwUnless: function(actual) {
|
||||||
|
return env.throwUnless(actual);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark a spec as pending, expectation results will be ignored.
|
* Mark a spec as pending, expectation results will be ignored.
|
||||||
* @name pending
|
* @name pending
|
||||||
|
|||||||
Reference in New Issue
Block a user