Skip to content

Commit

Permalink
test: improve tests for unhandled rejections (salesforce#4453)
Browse files Browse the repository at this point in the history
  • Loading branch information
nolanlawson authored Aug 9, 2024
1 parent 9d207fb commit 1c02af1
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 38 deletions.
37 changes: 37 additions & 0 deletions packages/@lwc/integration-karma/helpers/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,42 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) {
const expectConsoleCalls = createExpectConsoleCallsFunc(false);
const expectConsoleCallsDev = createExpectConsoleCallsFunc(true);

// Utility to handle unhandled rejections or errors without allowing Jasmine to handle them first.
// Captures both onunhandledrejection and onerror events, since you might want both depending on
// native vs synthetic lifecycle timing differences.
function catchUnhandledRejectionsAndErrors(onUnhandledRejectionOrError) {
let originalOnError;

const onError = (e) => {
e.preventDefault(); // Avoids logging to the console
onUnhandledRejectionOrError(e);
};

const onRejection = (e) => {
// Avoids logging the error to the console, except in Firefox sadly https://bugzilla.mozilla.org/1642147
e.preventDefault();
onUnhandledRejectionOrError(e.reason);
};

beforeEach(() => {
// Overriding window.onerror disables Jasmine's global error handler, so we can listen for errors
// ourselves. There doesn't seem to be a better way to disable Jasmine's behavior here.
// https:/jasmine/jasmine/pull/1860
originalOnError = window.onerror;
// Dummy onError because Jasmine tries to call it in case of a rejection:
// https:/jasmine/jasmine/blob/169a2a8/src/core/GlobalErrors.js#L104-L106
window.onerror = () => {};
window.addEventListener('error', onError);
window.addEventListener('unhandledrejection', onRejection);
});

afterEach(() => {
window.removeEventListener('error', onError);
window.removeEventListener('unhandledrejection', onRejection);
window.onerror = originalOnError;
});
}

// These values are based on the API versions in @lwc/shared/api-version
const apiFeatures = {
LOWERCASE_SCOPE_TOKENS: process.env.API_VERSION >= 59,
Expand Down Expand Up @@ -630,6 +666,7 @@ window.TestUtils = (function (lwc, jasmine, beforeAll) {
IS_SYNTHETIC_SHADOW_LOADED,
expectConsoleCalls,
expectConsoleCallsDev,
catchUnhandledRejectionsAndErrors,
...apiFeatures,
};
})(LWC, jasmine, beforeAll);
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createElement } from 'lwc';
import { catchUnhandledRejectionsAndErrors } from 'test-utils';
import XBoundaryChildConstructorThrow from 'x/boundaryChildConstructorThrow';
import XBoundaryChildConnectedThrow from 'x/boundaryChildConnectedThrow';
import XBoundaryChildRenderThrow from 'x/boundaryChildRenderThrow';
Expand Down Expand Up @@ -291,39 +292,16 @@ describe('errorCallback error caught by another errorCallback', () => {
// These tests are important because certain code paths are only hit when errorCallback throws an error
// after a value mutation. this causes flushRehydrationQueue to be called, which has a try/catch for this error.
describe('errorCallback throws after value mutation', () => {
let originalOnError;
let caughtError;

// Depending on whether native custom elements lifecycle is enabled or not, this may be an unhandled error or an
// unhandled rejection
const onError = (e) => {
e.preventDefault(); // Avoids logging to the console
caughtError = e;
};

const onRejection = (e) => {
// Avoids logging the error to the console, except in Firefox sadly https://bugzilla.mozilla.org/1642147
e.preventDefault();
caughtError = e.reason;
};

beforeEach(() => {
// Overriding window.onerror disables Jasmine's global error handler, so we can listen for errors
// ourselves. There doesn't seem to be a better way to disable Jasmine's behavior here.
// https:/jasmine/jasmine/pull/1860
originalOnError = window.onerror;
// Dummy onError because Jasmine tries to call it in case of a rejection:
// https:/jasmine/jasmine/blob/169a2a8/src/core/GlobalErrors.js#L104-L106
window.onerror = () => {};
caughtError = undefined;
window.addEventListener('error', onError);
window.addEventListener('unhandledrejection', onRejection);
// unhandled rejection. This utility captures both.
catchUnhandledRejectionsAndErrors((error) => {
caughtError = error;
});

afterEach(() => {
window.removeEventListener('error', onError);
window.removeEventListener('unhandledrejection', onRejection);
window.onerror = originalOnError;
caughtError = undefined;
});

function testStub(testcase, hostSelector, hostClass, expectAfterThrowingChildToExist) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createElement } from 'lwc';
import { catchUnhandledRejectionsAndErrors } from 'test-utils';
import ShadowParent from 'x/shadowParent';
import ShadowLightParent from 'x/shadowLightParent';
import LightParent from 'x/lightParent';
Expand Down Expand Up @@ -296,22 +297,20 @@ it('should invoke callbacks on the right order when multiple templates are used
});

describe('regression test (#3827)', () => {
let originalOnError;
function onError(event) {
event.preventDefault(); // don't log the error
}
let caughtErrors;

beforeEach(() => {
// These error handlers are here to capture errors thrown in synthetic shadow mode
// after the rerendering happens.
window.onerror;
window.onerror = null;
window.addEventListener('error', onError);
caughtErrors = [];
});

// TODO [#4451]: synthetic shadow throws unhandled rejection errors
// These handlers capture errors thrown in synthetic shadow mode after the rerendering happens.
catchUnhandledRejectionsAndErrors((error) => {
caughtErrors.push(error);
});

afterEach(() => {
window.onerror = originalOnError;
window.removeEventListener('error', onError);
caughtErrors = undefined;
});

const fixtures = [
Expand Down Expand Up @@ -404,6 +403,23 @@ describe('regression test (#3827)', () => {
previousLeafName = currentLeafName;
currentLeafName = container.getLeaf().name;
expect(window.timingBuffer).toEqual(elseIfBlock(currentLeafName, previousLeafName));

// TODO [#4451]: synthetic shadow throws unhandled rejection errors
// Remove the element and wait two macrotasks - this is when the unhandled rejections occur
document.body.removeChild(container);
await new Promise((resolve) => setTimeout(resolve));
await new Promise((resolve) => setTimeout(resolve));

if (fixtureName === 'shadow DOM' && !process.env.NATIVE_SHADOW) {
expect(caughtErrors.length).toBe(2);
for (const caughtError of caughtErrors) {
expect(caughtError.message).toMatch(
/The node to be removed is not a child of this node|The object can not be found here/
);
}
} else {
expect(caughtErrors.length).toBe(0);
}
});
});
});

0 comments on commit 1c02af1

Please sign in to comment.