/*** 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';
const React = require('react');
const ReactDOM = require('react-dom');
const stripEmptyValues = function (obj) {
const ret = {};
for (const name in obj) {
if (!obj.hasOwnProperty(name)) {
continue;
}if (obj[name] !== null && obj[name] !== undefined) {
ret[name] = obj[name];
}}return ret;
};let idCounter = 123;
/*** Contains internal static internal state in order to test that updates to* existing children won't reinitialize components, when moving children -* reusing existing DOM/memory resources.*/class StatusDisplay extends React.Component {
state = {internalState: idCounter++};
getStatus() {
return this.props.status;
}getInternalState() {
return this.state.internalState;
}componentDidMount() {
this.props.onFlush();
}componentDidUpdate() {
this.props.onFlush();
}render() {
return <div>{this.props.contentKey}</div>;
}}/*** Displays friends statuses.
*/class FriendsStatusDisplay extends React.Component {
displays = {};/**
* Gets the order directly from each rendered child's `index` field.
* Refs are not maintained in the rendered order, and neither is
* `this._renderedChildren` (surprisingly).
*/getOriginalKeys() {const originalKeys = [];
for (const key in this.props.usernameToStatus) {
if (this.props.usernameToStatus[key]) {
originalKeys.push(key);
}}return originalKeys;}/**
* Retrieves the rendered children in a nice format for comparing to the input
* `this.props.usernameToStatus`.
*/getStatusDisplays() {const res = {};const originalKeys = this.getOriginalKeys();
for (let i = 0; i < originalKeys.length; i++) {
const key = originalKeys[i];
res[key] = this.displays[key];
}return res;}/**
* Verifies that by the time a child is flushed, the refs that appeared
* earlier have already been resolved.
* TODO: This assumption will likely break with incremental reconciler
* but our internal layer API depends on this assumption. We need to change
* it to be more declarative before making ref resolution indeterministic.
*/verifyPreviousRefsResolved(flushedKey) {const originalKeys = this.getOriginalKeys();
for (let i = 0; i < originalKeys.length; i++) {
const key = originalKeys[i];
if (key === flushedKey) {// We are only interested in children up to the current key.
return;
}expect(this.displays[key]).toBeTruthy();
}}render() {
const children = [];
for (const key in this.props.usernameToStatus) {
const status = this.props.usernameToStatus[key];
children.push(
!status ? null : (
<StatusDisplay
key={key}
ref={current => {
this.displays[key] = current;
}}contentKey={key}
onFlush={this.verifyPreviousRefsResolved.bind(this, key)}
status={status}
/>),
);
}const childrenToRender = this.props.prepareChildren(children);
return <div>{childrenToRender}</div>;
}}function getInternalStateByUserName(statusDisplays) {return Object.keys(statusDisplays).reduce((acc, key) => {
acc[key] = statusDisplays[key].getInternalState();
return acc;}, {});}/*** Verifies that the rendered `StatusDisplay` instances match the `props` that
* were responsible for allocating them. Checks the content of the user's status
* message as well as the order of them.
*/function verifyStatuses(statusDisplays, props) {const nonEmptyStatusDisplays = stripEmptyValues(statusDisplays);const nonEmptyStatusProps = stripEmptyValues(props.usernameToStatus);
let username;expect(Object.keys(nonEmptyStatusDisplays).length).toEqual(
Object.keys(nonEmptyStatusProps).length,
);for (username in nonEmptyStatusDisplays) {if (!nonEmptyStatusDisplays.hasOwnProperty(username)) {
continue;}expect(nonEmptyStatusDisplays[username].getStatus()).toEqual(
nonEmptyStatusProps[username],
);}// now go the other way to make sure we got them all.
for (username in nonEmptyStatusProps) {
if (!nonEmptyStatusProps.hasOwnProperty(username)) {
continue;}expect(nonEmptyStatusDisplays[username].getStatus()).toEqual(
nonEmptyStatusProps[username],
);}expect(Object.keys(nonEmptyStatusDisplays)).toEqual(
Object.keys(nonEmptyStatusProps),
);}/*** For all statusDisplays that existed in the previous iteration of the* sequence, verify that the state has been preserved. `StatusDisplay` contains* a unique number that allows us to track internal state across ordering* movements.*/function verifyStatesPreserved(lastInternalStates, statusDisplays) {
let key;
for (key in statusDisplays) {
if (!statusDisplays.hasOwnProperty(key)) {
continue;
}if (lastInternalStates[key]) {
expect(lastInternalStates[key]).toEqual(
statusDisplays[key].getInternalState(),
);}}}/*** Verifies that the internal representation of a set of `renderedChildren`* accurately reflects what is in the DOM.*/function verifyDomOrderingAccurate(outerContainer, statusDisplays) {
const containerNode = outerContainer.firstChild;
const statusDisplayNodes = containerNode.childNodes;
const orderedDomKeys = [];
for (let i = 0; i < statusDisplayNodes.length; i++) {
const contentKey = statusDisplayNodes[i].textContent;
orderedDomKeys.push(contentKey);
}const orderedLogicalKeys = [];
let username;
for (username in statusDisplays) {
if (!statusDisplays.hasOwnProperty(username)) {
continue;
}const statusDisplay = statusDisplays[username];
orderedLogicalKeys.push(statusDisplay.props.contentKey);
}expect(orderedDomKeys).toEqual(orderedLogicalKeys);
}function testPropsSequenceWithPreparedChildren(sequence, prepareChildren) {
const container = document.createElement('div');
const parentInstance = ReactDOM.render(
<FriendsStatusDisplay {...sequence[0]} prepareChildren={prepareChildren} />,
container,
);let statusDisplays = parentInstance.getStatusDisplays();
let lastInternalStates = getInternalStateByUserName(statusDisplays);
verifyStatuses(statusDisplays, sequence[0]);
for (let i = 1; i < sequence.length; i++) {
ReactDOM.render(
<FriendsStatusDisplay
{...sequence[i]}
prepareChildren={prepareChildren}
/>,container,
);statusDisplays = parentInstance.getStatusDisplays();
verifyStatuses(statusDisplays, sequence[i]);
verifyStatesPreserved(lastInternalStates, statusDisplays);
verifyDomOrderingAccurate(container, statusDisplays);
lastInternalStates = getInternalStateByUserName(statusDisplays);
}}function prepareChildrenArray(childrenArray) {
return childrenArray;
}function prepareChildrenLegacyIterable(childrenArray) {
return {
'@@iterator': function* () {
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const child of childrenArray) {
yield child;
}},};}function prepareChildrenModernIterable(childrenArray) {
return {
[Symbol.iterator]: function* () {
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const child of childrenArray) {
yield child;
}},};}function testPropsSequence(sequence) {
testPropsSequenceWithPreparedChildren(sequence, prepareChildrenArray);
testPropsSequenceWithPreparedChildren(
sequence,
prepareChildrenLegacyIterable,
);testPropsSequenceWithPreparedChildren(
sequence,
prepareChildrenModernIterable,
);}describe('ReactMultiChildReconcile', () => {
beforeEach(() => {
jest.resetModules();
});it('should reset internal state if removed then readded in an array', () => {
// Test basics.
const props = {
usernameToStatus: {jcw: 'jcwStatus',
},};const container = document.createElement('div');
const parentInstance = ReactDOM.render(
<FriendsStatusDisplay
{...props}
prepareChildren={prepareChildrenArray}
/>,container,
);let statusDisplays = parentInstance.getStatusDisplays();
const startingInternalState = statusDisplays.jcw.getInternalState();
// Now remove the child.
ReactDOM.render(
<FriendsStatusDisplay prepareChildren={prepareChildrenArray} />,
container,
);statusDisplays = parentInstance.getStatusDisplays();
expect(statusDisplays.jcw).toBeFalsy();
// Now reset the props that cause there to be a child
ReactDOM.render(
<FriendsStatusDisplay
{...props}
prepareChildren={prepareChildrenArray}
/>,container,
);statusDisplays = parentInstance.getStatusDisplays();
expect(statusDisplays.jcw).toBeTruthy();
expect(statusDisplays.jcw.getInternalState()).not.toBe(
startingInternalState,
);});it('should reset internal state if removed then readded in a legacy iterable', () => {
// Test basics.
const props = {
usernameToStatus: {jcw: 'jcwStatus',
},};const container = document.createElement('div');
const parentInstance = ReactDOM.render(
<FriendsStatusDisplay
{...props}
prepareChildren={prepareChildrenLegacyIterable}
/>,container,
);let statusDisplays = parentInstance.getStatusDisplays();
const startingInternalState = statusDisplays.jcw.getInternalState();
// Now remove the child.
ReactDOM.render(
<FriendsStatusDisplay prepareChildren={prepareChildrenLegacyIterable} />,
container,
);statusDisplays = parentInstance.getStatusDisplays();
expect(statusDisplays.jcw).toBeFalsy();
// Now reset the props that cause there to be a child
ReactDOM.render(
<FriendsStatusDisplay
{...props}
prepareChildren={prepareChildrenLegacyIterable}
/>,container,
);statusDisplays = parentInstance.getStatusDisplays();
expect(statusDisplays.jcw).toBeTruthy();
expect(statusDisplays.jcw.getInternalState()).not.toBe(
startingInternalState,
);});it('should reset internal state if removed then readded in a modern iterable', () => {
// Test basics.
const props = {
usernameToStatus: {jcw: 'jcwStatus',
},};const container = document.createElement('div');
const parentInstance = ReactDOM.render(
<FriendsStatusDisplay
{...props}
prepareChildren={prepareChildrenModernIterable}
/>,container,
);let statusDisplays = parentInstance.getStatusDisplays();
const startingInternalState = statusDisplays.jcw.getInternalState();
// Now remove the child.
ReactDOM.render(
<FriendsStatusDisplay prepareChildren={prepareChildrenModernIterable} />,
container,
);statusDisplays = parentInstance.getStatusDisplays();
expect(statusDisplays.jcw).toBeFalsy();
// Now reset the props that cause there to be a child
ReactDOM.render(
<FriendsStatusDisplay
{...props}
prepareChildren={prepareChildrenModernIterable}
/>,container,
);statusDisplays = parentInstance.getStatusDisplays();
expect(statusDisplays.jcw).toBeTruthy();
expect(statusDisplays.jcw.getInternalState()).not.toBe(
startingInternalState,
);});it('should create unique identity', () => {
// Test basics.
const usernameToStatus = {
jcw: 'jcwStatus',
awalke: 'awalkeStatus',
bob: 'bobStatus',
};testPropsSequence([{usernameToStatus: usernameToStatus}]);
});it('should preserve order if children order has not changed', () => {
const PROPS_SEQUENCE = [
{usernameToStatus: {jcw: 'jcwStatus',
jordanjcw: 'jordanjcwStatus',
},},{usernameToStatus: {jcw: 'jcwstatus2',
jordanjcw: 'jordanjcwstatus2',
},},];testPropsSequence(PROPS_SEQUENCE);
});it('should transition from zero to one children correctly', () => {
const PROPS_SEQUENCE = [
{usernameToStatus: {}},{usernameToStatus: {first: 'firstStatus',
},},];testPropsSequence(PROPS_SEQUENCE);
});it('should transition from one to zero children correctly', () => {
const PROPS_SEQUENCE = [
{usernameToStatus: {first: 'firstStatus',
},},{usernameToStatus: {}},];testPropsSequence(PROPS_SEQUENCE);
});it('should transition from one child to null children', () => {
testPropsSequence([
{usernameToStatus: {first: 'firstStatus',
},},{},]);});it('should transition from null children to one child', () => {
testPropsSequence([
{},{usernameToStatus: {first: 'firstStatus',
},},]);});it('should transition from zero children to null children', () => {
testPropsSequence([
{usernameToStatus: {},},{},]);});it('should transition from null children to zero children', () => {
testPropsSequence([
{},{usernameToStatus: {},},]);});/**
* `FriendsStatusDisplay` renders nulls as empty children (it's a convention* of `FriendsStatusDisplay`, nothing related to React or these test cases.*/it('should remove nulled out children at the beginning', () => {
const PROPS_SEQUENCE = [
{usernameToStatus: {jcw: 'jcwStatus',
jordanjcw: 'jordanjcwStatus',
},},{usernameToStatus: {jcw: null,
jordanjcw: 'jordanjcwstatus2',
},},];testPropsSequence(PROPS_SEQUENCE);
});it('should remove nulled out children at the end', () => {
const PROPS_SEQUENCE = [
{usernameToStatus: {jcw: 'jcwStatus',
jordanjcw: 'jordanjcwStatus',
},},{usernameToStatus: {jcw: 'jcwstatus2',
jordanjcw: null,
},},];testPropsSequence(PROPS_SEQUENCE);
});it('should reverse the order of two children', () => {
const PROPS_SEQUENCE = [
{usernameToStatus: {userOne: 'userOneStatus',
userTwo: 'userTwoStatus',
},},{usernameToStatus: {userTwo: 'userTwoStatus',
userOne: 'userOneStatus',
},},];testPropsSequence(PROPS_SEQUENCE);
});it('should reverse the order of more than two children', () => {
const PROPS_SEQUENCE = [
{usernameToStatus: {userOne: 'userOneStatus',
userTwo: 'userTwoStatus',
userThree: 'userThreeStatus',
},},{usernameToStatus: {userThree: 'userThreeStatus',
userTwo: 'userTwoStatus',
userOne: 'userOneStatus',
},},];testPropsSequence(PROPS_SEQUENCE);
});it('should cycle order correctly', () => {
const PROPS_SEQUENCE = [
{usernameToStatus: {userOne: 'userOneStatus',
userTwo: 'userTwoStatus',
userThree: 'userThreeStatus',
userFour: 'userFourStatus',
},},{usernameToStatus: {userTwo: 'userTwoStatus',
userThree: 'userThreeStatus',
userFour: 'userFourStatus',
userOne: 'userOneStatus',
},},{usernameToStatus: {userThree: 'userThreeStatus',
userFour: 'userFourStatus',
userOne: 'userOneStatus',
userTwo: 'userTwoStatus',
},},{usernameToStatus: {userFour: 'userFourStatus',
userOne: 'userOneStatus',
userTwo: 'userTwoStatus',
userThree: 'userThreeStatus',
},},{usernameToStatus: {// Full circle!
userOne: 'userOneStatus',
userTwo: 'userTwoStatus',
userThree: 'userThreeStatus',
userFour: 'userFourStatus',
},},];testPropsSequence(PROPS_SEQUENCE);
});it('should cycle order correctly in the other direction', () => {
const PROPS_SEQUENCE = [
{usernameToStatus: {userOne: 'userOneStatus',
userTwo: 'userTwoStatus',
userThree: 'userThreeStatus',
userFour: 'userFourStatus',
},},{usernameToStatus: {userFour: 'userFourStatus',
userOne: 'userOneStatus',
userTwo: 'userTwoStatus',
userThree: 'userThreeStatus',
},},{usernameToStatus: {userThree: 'userThreeStatus',
userFour: 'userFourStatus',
userOne: 'userOneStatus',
userTwo: 'userTwoStatus',
},},{usernameToStatus: {userTwo: 'userTwoStatus',
userThree: 'userThreeStatus',
userFour: 'userFourStatus',
userOne: 'userOneStatus',
},},{usernameToStatus: {// Full circle!
userOne: 'userOneStatus',
userTwo: 'userTwoStatus',
userThree: 'userThreeStatus',
userFour: 'userFourStatus',
},},];testPropsSequence(PROPS_SEQUENCE);
});it('should remove nulled out children and ignore new null children', () => {
const PROPS_SEQUENCE = [
{usernameToStatus: {jcw: 'jcwStatus',
jordanjcw: 'jordanjcwStatus',
},},{usernameToStatus: {jordanjcw: 'jordanjcwstatus2',
jcw: null,
another: null,
},},];testPropsSequence(PROPS_SEQUENCE);
});it('should remove nulled out children and reorder remaining', () => {
const PROPS_SEQUENCE = [
{usernameToStatus: {jcw: 'jcwStatus',
jordanjcw: 'jordanjcwStatus',
john: 'johnStatus', // john will go away
joe: 'joeStatus',
},},{usernameToStatus: {jordanjcw: 'jordanjcwStatus',
joe: 'joeStatus',
jcw: 'jcwStatus',
},},];testPropsSequence(PROPS_SEQUENCE);
});it('should append children to the end', () => {
const PROPS_SEQUENCE = [
{usernameToStatus: {jcw: 'jcwStatus',
jordanjcw: 'jordanjcwStatus',
},},{usernameToStatus: {jcw: 'jcwStatus',
jordanjcw: 'jordanjcwStatus',
jordanjcwnew: 'jordanjcwnewStatus',
},},];testPropsSequence(PROPS_SEQUENCE);
});it('should append multiple children to the end', () => {
const PROPS_SEQUENCE = [
{usernameToStatus: {jcw: 'jcwStatus',
jordanjcw: 'jordanjcwStatus',
},},{usernameToStatus: {jcw: 'jcwStatus',
jordanjcw: 'jordanjcwStatus',
jordanjcwnew: 'jordanjcwnewStatus',
jordanjcwnew2: 'jordanjcwnewStatus2',
},},];testPropsSequence(PROPS_SEQUENCE);
});it('should prepend children to the beginning', () => {
const PROPS_SEQUENCE = [
{usernameToStatus: {jcw: 'jcwStatus',
jordanjcw: 'jordanjcwStatus',
},},{usernameToStatus: {newUsername: 'newUsernameStatus',
jcw: 'jcwStatus',
jordanjcw: 'jordanjcwStatus',
},},];testPropsSequence(PROPS_SEQUENCE);
});it('should prepend multiple children to the beginning', () => {
const PROPS_SEQUENCE = [
{usernameToStatus: {jcw: 'jcwStatus',
jordanjcw: 'jordanjcwStatus',
},},{usernameToStatus: {newNewUsername: 'newNewUsernameStatus',
newUsername: 'newUsernameStatus',
jcw: 'jcwStatus',
jordanjcw: 'jordanjcwStatus',
},},];testPropsSequence(PROPS_SEQUENCE);
});it('should not prepend an empty child to the beginning', () => {
const PROPS_SEQUENCE = [
{usernameToStatus: {jcw: 'jcwStatus',
jordanjcw: 'jordanjcwStatus',
},},{usernameToStatus: {emptyUsername: null,
jcw: 'jcwStatus',
jordanjcw: 'jordanjcwStatus',
},},];testPropsSequence(PROPS_SEQUENCE);
});it('should not append an empty child to the end', () => {
const PROPS_SEQUENCE = [
{usernameToStatus: {jcw: 'jcwStatus',
jordanjcw: 'jordanjcwStatus',
},},{usernameToStatus: {jcw: 'jcwStatus',
jordanjcw: 'jordanjcwStatus',
emptyUsername: null,
},},];testPropsSequence(PROPS_SEQUENCE);
});it('should not insert empty children in the middle', () => {
const PROPS_SEQUENCE = [
{usernameToStatus: {jcw: 'jcwStatus',
jordanjcw: 'jordanjcwStatus',
},},{usernameToStatus: {jcw: 'jcwstatus2',
skipOverMe: null,
skipOverMeToo: null,
definitelySkipOverMe: null,
jordanjcw: 'jordanjcwstatus2',
},},];testPropsSequence(PROPS_SEQUENCE);
});it('should insert one new child in the middle', () => {
const PROPS_SEQUENCE = [
{usernameToStatus: {jcw: 'jcwStatus',
jordanjcw: 'jordanjcwStatus',
},},{usernameToStatus: {jcw: 'jcwstatus2',
insertThis: 'insertThisStatus',
jordanjcw: 'jordanjcwstatus2',
},},];testPropsSequence(PROPS_SEQUENCE);
});it('should insert multiple new truthy children in the middle', () => {
const PROPS_SEQUENCE = [
{usernameToStatus: {jcw: 'jcwStatus',
jordanjcw: 'jordanjcwStatus',
},},{usernameToStatus: {jcw: 'jcwstatus2',
insertThis: 'insertThisStatus',
insertThisToo: 'insertThisTooStatus',
definitelyInsertThisToo: 'definitelyInsertThisTooStatus',
jordanjcw: 'jordanjcwstatus2',
},},];testPropsSequence(PROPS_SEQUENCE);
});it('should insert non-empty children in middle where nulls were', () => {
const PROPS_SEQUENCE = [
{usernameToStatus: {jcw: 'jcwStatus',
insertThis: null,
insertThisToo: null,
definitelyInsertThisToo: null,
jordanjcw: 'jordanjcwStatus',
},},{usernameToStatus: {jcw: 'jcwstatus2',
insertThis: 'insertThisStatus',
insertThisToo: 'insertThisTooStatus',
definitelyInsertThisToo: 'definitelyInsertThisTooStatus',
jordanjcw: 'jordanjcwstatus2',
},},];testPropsSequence(PROPS_SEQUENCE);
});});