/**
* 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.
*/
'use strict';
// Mock of the Native Hooks
// Map of viewTag -> {children: [childTag], parent: ?parentTag}
const roots = [];
const views = new Map();
function autoCreateRoot(tag) {
// Seriously, this is how we distinguish roots in RN.
if (!views.has(tag) && tag % 10 === 1) {
roots.push(tag);
views.set(tag, {
children: [],
parent: null,
props: {},
viewName: '<native root>',
});
}
}
function insertSubviewAtIndex(parent, child, index) {
const parentInfo = views.get(parent);
const childInfo = views.get(child);
if (childInfo.parent !== null) {
throw new Error(
`Inserting view ${child} ${JSON.stringify(
childInfo.props,
)} which already has parent`,
);
}
if (0 > index || index > parentInfo.children.length) {
throw new Error(
`Invalid index ${index} for children ${parentInfo.children}`,
);
}
parentInfo.children.splice(index, 0, child);
childInfo.parent = parent;
}
function removeChild(parent, child) {
const parentInfo = views.get(parent);
const childInfo = views.get(child);
const index = parentInfo.children.indexOf(child);
if (index < 0) {
throw new Error(`Missing view ${child} during removal`);
}
parentInfo.children.splice(index, 1);
childInfo.parent = null;
}
const RCTUIManager = {
__dumpHierarchyForJestTestsOnly: function () {
function dumpSubtree(tag, indent) {
const info = views.get(tag);
let out = '';
out +=
' '.repeat(indent) + info.viewName + ' ' + JSON.stringify(info.props);
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const child of info.children) {
out += '\n' + dumpSubtree(child, indent + 2);
}
return out;
}
return roots.map(tag => dumpSubtree(tag, 0)).join('\n');
},
clearJSResponder: jest.fn(),
createView: jest.fn(function createView(reactTag, viewName, rootTag, props) {
if (views.has(reactTag)) {
throw new Error(`Created two native views with tag ${reactTag}`);
}
views.set(reactTag, {
children: [],
parent: null,
props: props,
viewName: viewName,
});
}),
dispatchViewManagerCommand: jest.fn(),
sendAccessibilityEvent: jest.fn(),
setJSResponder: jest.fn(),
setChildren: jest.fn(function setChildren(parentTag, reactTags) {
autoCreateRoot(parentTag);
// Native doesn't actually check this but it seems like a good idea
if (views.get(parentTag).children.length !== 0) {
throw new Error(`Calling .setChildren on nonempty view ${parentTag}`);
}
// This logic ported from iOS (RCTUIManager.m)
reactTags.forEach((tag, i) => {
insertSubviewAtIndex(parentTag, tag, i);
});
}),
manageChildren: jest.fn(function manageChildren(
parentTag,
moveFromIndices = [],
moveToIndices = [],
addChildReactTags = [],
addAtIndices = [],
removeAtIndices = [],
) {
autoCreateRoot(parentTag);
// This logic ported from iOS (RCTUIManager.m)
if (moveFromIndices.length !== moveToIndices.length) {
throw new Error(
`Mismatched move indices ${moveFromIndices} and ${moveToIndices}`,
);
}
if (addChildReactTags.length !== addAtIndices.length) {
throw new Error(
`Mismatched add indices ${addChildReactTags} and ${addAtIndices}`,
);
}
const parentInfo = views.get(parentTag);
const permanentlyRemovedChildren = removeAtIndices.map(
index => parentInfo.children[index],
);
const temporarilyRemovedChildren = moveFromIndices.map(
index => parentInfo.children[index],
);
permanentlyRemovedChildren.forEach(tag => removeChild(parentTag, tag));
temporarilyRemovedChildren.forEach(tag => removeChild(parentTag, tag));
permanentlyRemovedChildren.forEach(tag => {
views.delete(tag);
});
// List of [index, tag]
const indicesToInsert = [];
temporarilyRemovedChildren.forEach((tag, i) => {
indicesToInsert.push([moveToIndices[i], temporarilyRemovedChildren[i]]);
});
addChildReactTags.forEach((tag, i) => {
indicesToInsert.push([addAtIndices[i], addChildReactTags[i]]);
});
indicesToInsert.sort((a, b) => a[0] - b[0]);
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const [i, tag] of indicesToInsert) {
insertSubviewAtIndex(parentTag, tag, i);
}
}),
updateView: jest.fn(),
removeSubviewsFromContainerWithID: jest.fn(function (parentTag) {
views.get(parentTag).children.forEach(tag => removeChild(parentTag, tag));
}),
replaceExistingNonRootView: jest.fn(),
measure: jest.fn(function measure(tag, callback) {
if (typeof tag !== 'number') {
throw new Error(`Expected tag to be a number, was passed ${tag}`);
}
callback(10, 10, 100, 100, 0, 0);
}),
measureInWindow: jest.fn(function measureInWindow(tag, callback) {
if (typeof tag !== 'number') {
throw new Error(`Expected tag to be a number, was passed ${tag}`);
}
callback(10, 10, 100, 100);
}),
measureLayout: jest.fn(
function measureLayout(tag, relativeTag, fail, success) {
if (typeof tag !== 'number') {
throw new Error(`Expected tag to be a number, was passed ${tag}`);
}
if (typeof relativeTag !== 'number') {
throw new Error(
`Expected relativeTag to be a number, was passed ${relativeTag}`,
);
}
success(1, 1, 100, 100);
},
),
__takeSnapshot: jest.fn(),
};
module.exports = RCTUIManager;