Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: improve tests for unhandled rejections #4453

Merged
merged 2 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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://github.com/jasmine/jasmine/pull/1860
originalOnError = window.onerror;
// Dummy onError because Jasmine tries to call it in case of a rejection:
// https://github.com/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;
});
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is 99% copypasta from LightningElement.errorCallback/index.spec.js

// 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://github.com/jasmine/jasmine/pull/1860
originalOnError = window.onerror;
// Dummy onError because Jasmine tries to call it in case of a rejection:
// https://github.com/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);
}
});
});
});
Loading