/**
* 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';
let React;
let ReactDOM;
let ReactTestUtils;
describe('ReactIdentity', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
ReactTestUtils = require('react-dom/test-utils');
});
it('should allow key property to express identity', () => {
let node;
const Component = props => (
<div ref={c => (node = c)}>
<div key={props.swap ? 'banana' : 'apple'} />
<div key={props.swap ? 'apple' : 'banana'} />
</div>
);
const container = document.createElement('div');
ReactDOM.render(<Component />, container);
const origChildren = Array.from(node.childNodes);
ReactDOM.render(<Component swap={true} />, container);
const newChildren = Array.from(node.childNodes);
expect(origChildren[0]).toBe(newChildren[1]);
expect(origChildren[1]).toBe(newChildren[0]);
});
it('should use composite identity', () => {
class Wrapper extends React.Component {
render() {
return <a>{this.props.children}</a>;
}
}
const container = document.createElement('div');
let node1;
let node2;
ReactDOM.render(
<Wrapper key="wrap1">
<span ref={c => (node1 = c)} />
</Wrapper>,
container,
);
ReactDOM.render(
<Wrapper key="wrap2">
<span ref={c => (node2 = c)} />
</Wrapper>,
container,
);
expect(node1).not.toBe(node2);
});
function renderAComponentWithKeyIntoContainer(key, container) {
class Wrapper extends React.Component {
spanRef = React.createRef();
render() {
return (
<div>
<span ref={this.spanRef} key={key} />
</div>
);
}
}
const instance = ReactDOM.render(<Wrapper />, container);
const span = instance.spanRef.current;
expect(span).not.toBe(null);
}
it('should allow any character as a key, in a detached parent', () => {
const detachedContainer = document.createElement('div');
renderAComponentWithKeyIntoContainer("<'WEIRD/&\\key'>", detachedContainer);
});
it('should allow any character as a key, in an attached parent', () => {
// This test exists to protect against implementation details that
// incorrectly query escaped IDs using DOM tools like getElementById.
const attachedContainer = document.createElement('div');
document.body.appendChild(attachedContainer);
renderAComponentWithKeyIntoContainer("<'WEIRD/&\\key'>", attachedContainer);
document.body.removeChild(attachedContainer);
});
it('should not allow scripts in keys to execute', () => {
const h4x0rKey =
'"><script>window[\'YOUVEBEENH4X0RED\']=true;</script><div id="';
const attachedContainer = document.createElement('div');
document.body.appendChild(attachedContainer);
renderAComponentWithKeyIntoContainer(h4x0rKey, attachedContainer);
document.body.removeChild(attachedContainer);
// If we get this far, make sure we haven't executed the code
expect(window.YOUVEBEENH4X0RED).toBe(undefined);
});
it('should let restructured components retain their uniqueness', () => {
const instance0 = <span />;
const instance1 = <span />;
const instance2 = <span />;
class TestComponent extends React.Component {
render() {
return (
<div>
{instance2}
{this.props.children[0]}
{this.props.children[1]}
</div>
);
}
}
class TestContainer extends React.Component {
render() {
return (
<TestComponent>
{instance0}
{instance1}
</TestComponent>
);
}
}
expect(function () {
ReactTestUtils.renderIntoDocument(<TestContainer />);
}).not.toThrow();
});
it('should let nested restructures retain their uniqueness', () => {
const instance0 = <span />;
const instance1 = <span />;
const instance2 = <span />;
class TestComponent extends React.Component {
render() {
return (
<div>
{instance2}
{this.props.children[0]}
{this.props.children[1]}
</div>
);
}
}
class TestContainer extends React.Component {
render() {
return (
<div>
<TestComponent>
{instance0}
{instance1}
</TestComponent>
</div>
);
}
}
expect(function () {
ReactTestUtils.renderIntoDocument(<TestContainer />);
}).not.toThrow();
});
it('should let text nodes retain their uniqueness', () => {
class TestComponent extends React.Component {
render() {
return (
<div>
{this.props.children}
<span />
</div>
);
}
}
class TestContainer extends React.Component {
render() {
return (
<TestComponent>
<div />
{'second'}
</TestComponent>
);
}
}
expect(function () {
ReactTestUtils.renderIntoDocument(<TestContainer />);
}).not.toThrow();
});
it('should retain key during updates in composite components', () => {
class TestComponent extends React.Component {
render() {
return <div>{this.props.children}</div>;
}
}
class TestContainer extends React.Component {
state = {swapped: false};
swap = () => {
this.setState({swapped: true});
};
render() {
return (
<TestComponent>
{this.state.swapped ? this.props.second : this.props.first}
{this.state.swapped ? this.props.first : this.props.second}
</TestComponent>
);
}
}
const instance0 = <span key="A" />;
const instance1 = <span key="B" />;
let wrapped = <TestContainer first={instance0} second={instance1} />;
wrapped = ReactDOM.render(wrapped, document.createElement('div'));
const div = ReactDOM.findDOMNode(wrapped);
const beforeA = div.childNodes[0];
const beforeB = div.childNodes[1];
wrapped.swap();
const afterA = div.childNodes[1];
const afterB = div.childNodes[0];
expect(beforeA).toBe(afterA);
expect(beforeB).toBe(afterB);
});
it('should not allow implicit and explicit keys to collide', () => {
const component = (
<div>
<span />
<span key="0" />
</div>
);
expect(function () {
ReactTestUtils.renderIntoDocument(component);
}).not.toThrow();
});
it('should throw if key is a Temporal-like object', () => {
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 el = document.createElement('div');
const test = () =>
ReactDOM.render(
<div>
<span key={new TemporalLike()} />
</div>,
el,
);
expect(() =>
expect(test).toThrowError(new TypeError('prod message')),
).toErrorDev(
'The provided key is an unsupported type TemporalLike.' +
' This value must be coerced to a string before using it here.',
{withoutStack: true},
);
});
});