mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Basically make `console.error` and `console.warn` behave like normal - when a component stack isn't appended. I need this because I need to be able to print rich logs with the component stack option and to be able to disable instrumentation completely in `console.createTask` environments that don't need it. Currently we can't print logs with richer objects because they're toString:ed first. In practice, pretty much all arguments we log are already toString:ed so it's not necessary anyway. Some might be like a number. So it would only be a problem if some environment can't handle proper consoles but then it's up to that environment to toString it before logging. The `Warning: ` prefix is historic and is both noisy and confusing. It's mostly unnecessary since the UI surrounding `console.error` and `console.warn` tend to have visual treatment around it anyway. However, it's actively misleading when `console.error` gets prefixed with a Warning that we consider an error level. There's an argument to be made that some of our `console.error` don't make the bar for an error but then the argument is to downgrade each of those to `console.warn` - not to brand all our actual error logging with `Warning: `. Apparently something needs to change in React Native before landing this because it depends on the prefix somehow which probably doesn't make sense already.
221 lines
7.1 KiB
JavaScript
221 lines
7.1 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @emails react-core
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
describe('ReactDOM unknown attribute', () => {
|
|
let React;
|
|
let ReactDOMClient;
|
|
let act;
|
|
|
|
beforeEach(() => {
|
|
jest.resetModules();
|
|
React = require('react');
|
|
ReactDOMClient = require('react-dom/client');
|
|
act = require('internal-test-utils').act;
|
|
});
|
|
|
|
async function testUnknownAttributeRemoval(givenValue) {
|
|
const el = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(el);
|
|
|
|
await act(() => {
|
|
root.render(<div unknown="something" />);
|
|
});
|
|
|
|
expect(el.firstChild.getAttribute('unknown')).toBe('something');
|
|
|
|
await act(() => {
|
|
root.render(<div unknown={givenValue} />);
|
|
});
|
|
|
|
expect(el.firstChild.hasAttribute('unknown')).toBe(false);
|
|
}
|
|
|
|
async function testUnknownAttributeAssignment(givenValue, expectedDOMValue) {
|
|
const el = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(el);
|
|
|
|
await act(() => {
|
|
root.render(<div unknown="something" />);
|
|
});
|
|
|
|
expect(el.firstChild.getAttribute('unknown')).toBe('something');
|
|
|
|
await act(() => {
|
|
root.render(<div unknown={givenValue} />);
|
|
});
|
|
|
|
expect(el.firstChild.getAttribute('unknown')).toBe(expectedDOMValue);
|
|
}
|
|
|
|
describe('unknown attributes', () => {
|
|
it('removes values null and undefined', async () => {
|
|
await testUnknownAttributeRemoval(null);
|
|
await testUnknownAttributeRemoval(undefined);
|
|
});
|
|
|
|
it('changes values true, false to null, and also warns once', async () => {
|
|
await expect(() => testUnknownAttributeAssignment(true, null)).toErrorDev(
|
|
'Received `true` for a non-boolean attribute `unknown`.\n\n' +
|
|
'If you want to write it to the DOM, pass a string instead: ' +
|
|
'unknown="true" or unknown={value.toString()}.\n' +
|
|
' in div (at **)',
|
|
);
|
|
await testUnknownAttributeAssignment(false, null);
|
|
});
|
|
|
|
it('removes unknown attributes that were rendered but are now missing', async () => {
|
|
const el = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(el);
|
|
|
|
await act(() => {
|
|
root.render(<div unknown="something" />);
|
|
});
|
|
|
|
expect(el.firstChild.getAttribute('unknown')).toBe('something');
|
|
|
|
await act(() => {
|
|
root.render(<div />);
|
|
});
|
|
|
|
expect(el.firstChild.hasAttribute('unknown')).toBe(false);
|
|
});
|
|
|
|
it('removes new boolean props', async () => {
|
|
const el = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(el);
|
|
|
|
await expect(async () => {
|
|
await act(() => {
|
|
root.render(<div inert={true} />);
|
|
});
|
|
}).toErrorDev([]);
|
|
|
|
expect(el.firstChild.getAttribute('inert')).toBe(true ? '' : null);
|
|
});
|
|
|
|
it('warns once for empty strings in new boolean props', async () => {
|
|
const el = document.createElement('div');
|
|
const root = ReactDOMClient.createRoot(el);
|
|
|
|
await expect(async () => {
|
|
await act(() => {
|
|
root.render(<div inert="" />);
|
|
});
|
|
}).toErrorDev([
|
|
'Received an empty string for a boolean attribute `inert`. ' +
|
|
'This will treat the attribute as if it were false. ' +
|
|
'Either pass `false` to silence this warning, or ' +
|
|
'pass `true` if you used an empty string in earlier versions of React to indicate this attribute is true.',
|
|
]);
|
|
|
|
expect(el.firstChild.getAttribute('inert')).toBe(true ? null : '');
|
|
|
|
// The warning is only printed once.
|
|
await act(() => {
|
|
root.render(<div inert="" />);
|
|
});
|
|
});
|
|
|
|
it('passes through strings', async () => {
|
|
await testUnknownAttributeAssignment('a string', 'a string');
|
|
});
|
|
|
|
it('coerces numbers to strings', async () => {
|
|
await testUnknownAttributeAssignment(0, '0');
|
|
await testUnknownAttributeAssignment(-1, '-1');
|
|
await testUnknownAttributeAssignment(42, '42');
|
|
await testUnknownAttributeAssignment(9000.99, '9000.99');
|
|
});
|
|
|
|
it('coerces NaN to strings and warns', async () => {
|
|
await expect(() => testUnknownAttributeAssignment(NaN, 'NaN')).toErrorDev(
|
|
'Received NaN for the `unknown` attribute. ' +
|
|
'If this is expected, cast the value to a string.\n' +
|
|
' in div (at **)',
|
|
);
|
|
});
|
|
|
|
it('coerces objects to strings and warns', async () => {
|
|
const lol = {
|
|
toString() {
|
|
return 'lol';
|
|
},
|
|
};
|
|
|
|
await testUnknownAttributeAssignment({hello: 'world'}, '[object Object]');
|
|
await testUnknownAttributeAssignment(lol, 'lol');
|
|
});
|
|
|
|
it('throws with Temporal-like objects', async () => {
|
|
class TemporalLike {
|
|
valueOf() {
|
|
// Throwing here is the behavior of ECMAScript "Temporal" date/time API.
|
|
// See https://tc39.es/proposal-temporal/docs/plaindate.html#valueOf
|
|
throw new TypeError('prod message');
|
|
}
|
|
toString() {
|
|
return '2020-01-01';
|
|
}
|
|
}
|
|
const test = () =>
|
|
testUnknownAttributeAssignment(new TemporalLike(), null);
|
|
await expect(() =>
|
|
expect(test).rejects.toThrowError(new TypeError('prod message')),
|
|
).toErrorDev(
|
|
'The provided `unknown` attribute is an unsupported type TemporalLike.' +
|
|
' This value must be coerced to a string before using it here.',
|
|
);
|
|
});
|
|
|
|
it('removes symbols and warns', async () => {
|
|
await expect(() => testUnknownAttributeRemoval(Symbol('foo'))).toErrorDev(
|
|
'Invalid value for prop `unknown` on <div> tag. Either remove it ' +
|
|
'from the element, or pass a string or number value to keep it ' +
|
|
'in the DOM. For details, see https://react.dev/link/attribute-behavior \n' +
|
|
' in div (at **)',
|
|
);
|
|
});
|
|
|
|
it('removes functions and warns', async () => {
|
|
await expect(() =>
|
|
testUnknownAttributeRemoval(function someFunction() {}),
|
|
).toErrorDev(
|
|
'Invalid value for prop `unknown` on <div> tag. Either remove ' +
|
|
'it from the element, or pass a string or number value to ' +
|
|
'keep it in the DOM. For details, see ' +
|
|
'https://react.dev/link/attribute-behavior \n' +
|
|
' in div (at **)',
|
|
);
|
|
});
|
|
|
|
it('allows camelCase unknown attributes and warns', async () => {
|
|
const el = document.createElement('div');
|
|
|
|
await expect(async () => {
|
|
const root = ReactDOMClient.createRoot(el);
|
|
|
|
await act(() => {
|
|
root.render(<div helloWorld="something" />);
|
|
});
|
|
}).toErrorDev(
|
|
'React does not recognize the `helloWorld` prop on a DOM element. ' +
|
|
'If you intentionally want it to appear in the DOM as a custom ' +
|
|
'attribute, spell it as lowercase `helloworld` instead. ' +
|
|
'If you accidentally passed it from a parent component, remove ' +
|
|
'it from the DOM element.\n' +
|
|
' in div (at **)',
|
|
);
|
|
|
|
expect(el.firstChild.getAttribute('helloworld')).toBe('something');
|
|
});
|
|
});
|
|
});
|