forked from qunitjs/qunit
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
== Background == Previously, QUnit.onError and QUnit.onUnhandledRejection could report global errors by synthesizing a new test, even after a run has ended. This is problematic when an errors ocurrs after all modules (and their hooks) have finished, and the overall test run has ended. The most immediate problem is that hooks having finished already, means it is illegal for a new test to start since "after" has already run. To protect against such illegal calls, the hooks object is emptied internally, and this new test causes an internal error: ``` TypeError: Cannot read property 'length' of undefined ``` This is not underlying problem though, but rather our internal safeguard working as intended. The higher-level problem is that there is no appropiate way to report a late error as a test since the run has already ended. The `QUnit.done()` callbacks have run, and the `runEnd` event has been emitted. == Approach == Instead of trying to report (late) errors as a test, only print them to `console.warn()`, which goes to stderr in Node.js. For the CLI, also remember that uncaught errors were found and use that to make sure we don't change exitCode back to zero (e.g. in case we have an uncaught error after the last test but before our `runEnd` callback is called). == Changes == * Generalise `QUnit.onUnhandledRejection` and re-use it for `window.onerror` (browser), and uncaught exceptions (CLI). * Fix broken use of `QUnit.onError` in `process.on( "uncaughtException" )`. This was passing the wrong parameters. Use the new onUncaughtException method instead. * Clarify that `QUnit.onError` is only for `window.onerror`. For now, keep its strange non-standard signature as-is (with the custom object parameter), but document this and its return value. * Remove the unused "..args" from `QUnit.onError`. This was only ever passed from one of our unit tests to give one extra argument (a string of "actual"), which then ended up passed as "actual" parameter to `pushFailure()`. We never used this in the actual onError binding, so remove this odd variadic construct for now. * Change `ProcessingQueue#done`, which is in charge of reporting the "No tests were run" error, to no longer rely on the way that `QUnit.onError` previously queued a late test. The first part of this function may run twice (same as before, once after an empty test run, and one more time after the synthetic test has finished and the queue is empty again). Change this so that we no longer assign `finished = true` in that first part. This means we will still support queueing of this one late test. But, since the quueue is empty, we do need to call `advance()` manually as otherwise it'd never get processed. Previously, `finished = true` was assigned first, which meant that `QUnit.onError` was adding a test under that condition. But this worked anyway because `Test#queue` internally had manual advancing exactly for this use case, which is also where we now emit a deprecation warning (to become an error in QUnit 3). Note that using this for anything other than the "No tests run" error was already unreliable since generally runEnd would have been emitted already. The "No tests run" test was exactly done from the one sweet spot where it was (and remains) safe because that threw an error and thus prevented runEnd from being emitted. Fixes qunitjs#1377. Ref qunitjs#1322. Ref qunitjs#1446.
- Loading branch information
Showing
18 changed files
with
304 additions
and
209 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
--- | ||
layout: default | ||
title: QUnit.onUncaughtException() | ||
excerpt: Handle a global error. | ||
categories: | ||
- config | ||
version_added: "unversioned" | ||
--- | ||
|
||
`QUnit.onUncaughtException( error )` | ||
|
||
Handle a global error that should result in a failed test run. | ||
|
||
| name | description | | ||
|------|-------------| | ||
| `error` (any) | Usually an `Error` object, but any other thrown or rejected value may be given as well. | | ||
|
||
### Examples | ||
|
||
```js | ||
const error = new Error( "Failed to reverse the polarity of the neutron flow" ); | ||
QUnit.onUncaughtException( error ); | ||
``` | ||
|
||
```js | ||
process.on( "uncaughtException", QUnit.onUncaughtException ); | ||
``` | ||
|
||
```js | ||
window.addEventListener( "unhandledrejection", function( event ) { | ||
QUnit.onUncaughtException( event.reason ); | ||
} ); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import config from "./config"; | ||
import ProcessingQueue from "./processing-queue"; | ||
import { sourceFromStacktrace } from "./stacktrace"; | ||
import { extend, errorString } from "./utilities"; | ||
import Logger from "../logger"; | ||
import { test } from "../test"; | ||
|
||
/** | ||
* Handle a global error that should result in a failed test run. | ||
* | ||
* Summary: | ||
* | ||
* - If there is a current test, it becomes a failed assertion. | ||
* - If there is a current module, it becomes a failed test (and bypassing filters). | ||
* Note that if we're before any other test or module, it will naturally | ||
* become a global test. | ||
* - If the overall test run has ended, the error is printed to `console.warn()`. | ||
* | ||
* @since 2.17.0 | ||
* @param {Error|any} error | ||
*/ | ||
export default function onUncaughtException( error ) { | ||
const message = errorString( error ); | ||
|
||
// We could let callers specify an extra offset to add to the number passed to | ||
// sourceFromStacktrace, in case they are a wrapper further away from the error | ||
// handler, and thus reduce some noise in the stack trace. However, we're not | ||
// doing this right now because it would almost never be used in practice given | ||
// the vast majority of error values will be an Error object, and thus have a | ||
// clean stack trace already. | ||
const source = error.stack || sourceFromStacktrace( 2 ); | ||
|
||
if ( config.current ) { | ||
config.current.assert.pushResult( { | ||
result: false, | ||
message: `global failure: ${message}`, | ||
source | ||
} ); | ||
} else if ( !ProcessingQueue.finished ) { | ||
test( "global failure", extend( function( assert ) { | ||
assert.pushResult( { result: false, message, source } ); | ||
}, { validTest: true } ) ); | ||
} else { | ||
|
||
// TODO: Once supported in js-reporters and QUnit, use a TAP "bail" event. | ||
// The CLI runner can use this to ensure a non-zero exit code, even if | ||
// emitted after "runEnd" and before the process exits. | ||
// The HTML Reporter can use this to renmder it on the page in a test-like | ||
// block for easy discovery. | ||
Logger.warn( `${message}\n${source}` ); | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,31 +1,33 @@ | ||
import { pushFailure, test } from "../test"; | ||
|
||
import config from "./config"; | ||
import { extend } from "./utilities"; | ||
import onUncaughtException from "./on-uncaught-exception"; | ||
|
||
// Handle an unhandled exception. By convention, returns true if further | ||
// error handling should be suppressed and false otherwise. | ||
// In this case, we will only suppress further error handling if the | ||
// "ignoreGlobalErrors" configuration option is enabled. | ||
export default function onError( error, ...args ) { | ||
if ( config.current ) { | ||
if ( config.current.ignoreGlobalErrors ) { | ||
return true; | ||
} | ||
pushFailure( | ||
error.message, | ||
error.stacktrace || error.fileName + ":" + error.lineNumber, | ||
...args | ||
); | ||
} else { | ||
test( "global failure", extend( function() { | ||
pushFailure( | ||
error.message, | ||
error.stacktrace || error.fileName + ":" + error.lineNumber, | ||
...args | ||
); | ||
}, { validTest: true } ) ); | ||
/** | ||
* Handle a window.onerror error. | ||
* | ||
* If there is a current test that sets the internal `ignoreGlobalErrors` field | ||
* (such as during `assert.throws()`), then the error is ignored and native | ||
* error reporting is suppressed as well. This is because in browsers, an error | ||
* can sometimes end up in `window.onerror` instead of in the local try/catch. | ||
* This ignoring of errors does not apply to our general onUncaughtException | ||
* method, nor to our `unhandledRejection` handlers, as those are not meant | ||
* to receive an "expected" error during `assert.throws()`. | ||
* | ||
* @see <https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror> | ||
* @param {Object} details | ||
* @param {string} details.message | ||
* @param {string} details.fileName | ||
* @param {number} details.lineNumber | ||
* @param {string|undefined} [details.stacktrace] | ||
* @return {bool} True if native error reporting should be suppressed. | ||
*/ | ||
export default function onWindowError( details ) { | ||
if ( config.current && config.current.ignoreGlobalErrors ) { | ||
return true; | ||
} | ||
|
||
const err = new Error( details.message ); | ||
err.stack = details.stacktrace || details.fileName + ":" + details.lineNumber; | ||
onUncaughtException( err ); | ||
|
||
return false; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.