Skip to content
Open
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
26 changes: 25 additions & 1 deletion packages/dom/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,31 @@ export function handleErrors(error, prefixMessage, element = null, additionalDat
let message = error.message;
message += `\n${prefixMessage} \n${JSON.stringify(additionalData)}`;
message += '\n Please validate that your DOM is as per W3C standards using any online tool';
error.message = message;

// `message` is not writable on every error shape. DOMException — thrown by
// `canvas.toDataURL()` on a tainted canvas, by CSSOM access on a cross-origin
// stylesheet, and by other DOM APIs — declares `message` as a getter-only
// accessor (WebIDL `readonly attribute`). Assigning to it inside this strict-mode
// bundle throws "Cannot set property message of #<DOMException> which has only a
// getter", which replaces the real, actionable error with a confusing TypeError
// and fails the whole snapshot. Frozen and sealed errors behave the same way.
// Enrich in place when we can; otherwise carry the enriched message on a new
// Error that preserves the original's name and a reference to the cause.
try {
error.message = message;
} catch {
// assignment threw — fall through to the wrapper below
}

// Also covers sloppy-mode callers, where the assignment fails silently.
if (error.message !== message) {
let wrapped = new Error(message);
wrapped.name = error.name;
wrapped.cause = error;
wrapped.handled = true;
throw wrapped;
}

error.handled = true;
throw error;
}
58 changes: 57 additions & 1 deletion packages/dom/test/utils.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,61 @@
import { resourceFromDataURL, resourceFromText, rewriteLocalhostURL, styleSheetFromNode } from '../src/utils';
import { handleErrors, resourceFromDataURL, resourceFromText, rewriteLocalhostURL, styleSheetFromNode } from '../src/utils';
describe('utils', () => {
describe('handleErrors', () => {
it('enriches the message in place on a plain Error', () => {
let original = new Error('boom');

expect(() => handleErrors(original, 'Error serializing thing: '))
.toThrowMatching(err => err === original &&
err.message.startsWith('boom') &&
err.message.includes('Error serializing thing:') &&
err.handled === true);
});

it('includes element data when an element is passed', () => {
let el = document.createElement('canvas');
el.className = 'chart';
el.id = 'sales';

expect(() => handleErrors(new Error('boom'), 'Error serializing canvas element: ', el))
.toThrowMatching(err => err.message.includes('"nodeName":"CANVAS"') &&
err.message.includes('"classNames":"chart"') &&
err.message.includes('"id":"sales"'));
});

// DOMException declares `message` as a getter-only accessor, so assigning to it
// in this strict-mode bundle throws a TypeError that masks the real error and
// fails the entire snapshot. Regression test for PER-10368.
it('does not throw a TypeError when the error message is getter-only', () => {
let original = new window.DOMException('The canvas has been tainted by cross-origin data.', 'SecurityError');

expect(() => handleErrors(original, 'Error serializing canvas element: '))
.toThrowMatching(err => !(err instanceof TypeError) &&
!err.message.includes('which has only a getter'));
});

it('preserves the original message, name, and cause when message is getter-only', () => {
let original = new window.DOMException('The canvas has been tainted by cross-origin data.', 'SecurityError');

expect(() => handleErrors(original, 'Error serializing canvas element: '))
.toThrowMatching(err => err !== original &&
err.name === 'SecurityError' &&
err.cause === original &&
err.handled === true &&
err.message.startsWith('The canvas has been tainted by cross-origin data.') &&
err.message.includes('Error serializing canvas element:') &&
err.message.includes('W3C standards'));
});

it('handles a frozen error without throwing a TypeError', () => {
let original = Object.freeze(new Error('frozen boom'));

expect(() => handleErrors(original, 'Error cloning node: '))
.toThrowMatching(err => !(err instanceof TypeError) &&
err.message.startsWith('frozen boom') &&
err.handled === true);
});
});

describe('styleSheetFromNode', () => {
it('creates stylesheet properly', () => {
const node = document.createElement('style');
Expand Down
Loading