React 测试 Selectors
该文件包含了测试用方法。
二、创建组件选择器
export function createComponentSelector(
component: component(),
): ComponentSelector {
return {
$$typeof: COMPONENT_TYPE,
value: component,
};
}
三、创建伪类选择器
export function createHasPseudoClassSelector(
selectors: Array<Selector>,
): HasPseudoClassSelector {
return {
$$typeof: HAS_PSEUDO_CLASS_TYPE,
value: selectors,
};
}
四、创建角色选择器
export function createRoleSelector(role: string): RoleSelector {
return {
$$typeof: ROLE_TYPE,
value: role,
};
}
五、创建文本选择器
export function createTextSelector(text: string): TextSelector {
return {
$$typeof: TEXT_TYPE,
value: text,
};
}
六、创建测试名称选择器
export function createTestNameSelector(id: string): TestNameSelector {
return {
$$typeof: TEST_NAME_TYPE,
value: id,
};
}
七、查找所有节点
备注
isHiddenSubtree()由宿主环境提供
export function findAllNodes(
hostRoot: Instance,
selectors: Array<Selector>,
): Array<Instance> {
if (!supportsTestSelectors) {
throw new Error('Test selector API is not supported by this renderer.');
}
const root = findFiberRootForHostRoot(hostRoot);
const matchingFibers = findPaths(root, selectors);
const instanceRoots: Array<Instance> = [];
const stack = Array.from(matchingFibers);
let index = 0;
while (index < stack.length) {
const node = stack[index++] as any as Fiber;
const tag = node.tag;
if (
tag === HostComponent ||
tag === HostHoistable ||
tag === HostSingleton
) {
if (isHiddenSubtree(node)) {
continue;
}
instanceRoots.push(node.stateNode);
} else {
let child = node.child;
while (child !== null) {
stack.push(child);
child = child.sibling;
}
}
}
return instanceRoots;
}
八、查找宿主环境根的 Fiber 根
备注
isHiddenSubtree()由宿主环境提供
export function getFindAllNodesFailureDescription(
hostRoot: Instance,
selectors: Array<Selector>,
): string | null {
if (!supportsTestSelectors) {
throw new Error('Test selector API is not supported by this renderer.');
}
const root = findFiberRootForHostRoot(hostRoot);
let maxSelectorIndex: number = 0;
const matchedNames = [];
// The logic of this loop should be kept in sync with findPaths()
// 这个循环的逻辑应与 findPaths() 保持同步
const stack = [root, 0];
let index = 0;
while (index < stack.length) {
const fiber = stack[index++] as any as Fiber;
const tag = fiber.tag;
let selectorIndex = stack[index++] as any as number;
const selector = selectors[selectorIndex];
if (
(tag === HostComponent ||
tag === HostHoistable ||
tag === HostSingleton) &&
isHiddenSubtree(fiber)
) {
continue;
} else if (matchSelector(fiber, selector)) {
matchedNames.push(selectorToString(selector));
selectorIndex++;
if (selectorIndex > maxSelectorIndex) {
maxSelectorIndex = selectorIndex;
}
}
if (selectorIndex < selectors.length) {
let child = fiber.child;
while (child !== null) {
stack.push(child, selectorIndex);
child = child.sibling;
}
}
}
if (maxSelectorIndex < selectors.length) {
const unmatchedNames = [];
for (let i = maxSelectorIndex; i < selectors.length; i++) {
unmatchedNames.push(selectorToString(selectors[i]));
}
return (
'findAllNodes was able to match part of the selector:\n' +
` ${matchedNames.join(' > ')}\n\n` +
'No matching component was found for:\n' +
` ${unmatchedNames.join(' > ')}`
);
}
return null;
}
九、查找边界矩形
备注
getBoundingRect()由宿主环境提供
export function findBoundingRects(
hostRoot: Instance,
selectors: Array<Selector>,
): Array<BoundingRect> {
if (!supportsTestSelectors) {
throw new Error('Test selector API is not supported by this renderer.');
}
const instanceRoots = findAllNodes(hostRoot, selectors);
const boundingRects: Array<BoundingRect> = [];
for (let i = 0; i < instanceRoots.length; i++) {
boundingRects.push(getBoundingRect(instanceRoots[i]));
}
for (let i = boundingRects.length - 1; i > 0; i--) {
const targetRect = boundingRects[i];
const targetLeft = targetRect.x;
const targetRight = targetLeft + targetRect.width;
const targetTop = targetRect.y;
const targetBottom = targetTop + targetRect.height;
for (let j = i - 1; j >= 0; j--) {
if (i !== j) {
const otherRect = boundingRects[j];
const otherLeft = otherRect.x;
const otherRight = otherLeft + otherRect.width;
const otherTop = otherRect.y;
const otherBottom = otherTop + otherRect.height;
// Merging all rects to the minimums set would be complicated,
// but we can handle the most common cases:
// 1. completely overlapping rects
// 2. adjacent rects that are the same width or height (e.g. items in a list)
//
// 将所有矩形合并到最小集合会很复杂,
// 但我们可以处理最常见的情况:
// 1. 完全重叠的矩形
// 2. 相邻且宽度或高度相同的矩形(例如列表中的项目)
//
// Even given the above constraints,
// we still won't end up with the fewest possible rects without doing multiple passes,
// but it's good enough for this purpose.
//
// 即使考虑上述限制,
// 如果不进行多次遍历,我们仍然无法得到最少的矩形,
// 但对于这个用途来说足够了。
if (
targetLeft >= otherLeft &&
targetTop >= otherTop &&
targetRight <= otherRight &&
targetBottom <= otherBottom
) {
// Complete overlapping rects; remove the inner one.
// 完全重叠的矩形;移除内部的那个。
boundingRects.splice(i, 1);
break;
} else if (
targetLeft === otherLeft &&
targetRect.width === otherRect.width &&
!(otherBottom < targetTop) &&
!(otherTop > targetBottom)
) {
// Adjacent vertical rects; merge them.
// 相邻的垂直矩形;将它们合并。
if (otherTop > targetTop) {
otherRect.height += otherTop - targetTop;
otherRect.y = targetTop;
}
if (otherBottom < targetBottom) {
otherRect.height = targetBottom - otherTop;
}
boundingRects.splice(i, 1);
break;
} else if (
targetTop === otherTop &&
targetRect.height === otherRect.height &&
!(otherRight < targetLeft) &&
!(otherLeft > targetRight)
) {
// Adjacent horizontal rects; merge them.
// 相邻的水平矩形;将它们合并。
if (otherLeft > targetLeft) {
otherRect.width += otherLeft - targetLeft;
otherRect.x = targetLeft;
}
if (otherRight < targetRight) {
otherRect.width = targetRight - otherLeft;
}
boundingRects.splice(i, 1);
break;
}
}
}
}
return boundingRects;
}
十、焦点内
备注
isHiddenSubtree()由宿主环境提供setFocusIfFocusable()由宿主环境提供
export function focusWithin(
hostRoot: Instance,
selectors: Array<Selector>,
): boolean {
if (!supportsTestSelectors) {
throw new Error('Test selector API is not supported by this renderer.');
}
const root = findFiberRootForHostRoot(hostRoot);
const matchingFibers = findPaths(root, selectors);
const stack = Array.from(matchingFibers);
let index = 0;
while (index < stack.length) {
const fiber = stack[index++] as any as Fiber;
const tag = fiber.tag;
if (isHiddenSubtree(fiber)) {
continue;
}
if (
tag === HostComponent ||
tag === HostHoistable ||
tag === HostSingleton
) {
const node = fiber.stateNode;
if (setFocusIfFocusable(node)) {
return true;
}
}
let child = fiber.child;
while (child !== null) {
stack.push(child);
child = child.sibling;
}
}
return false;
}
十一、在提交根节点时
export function onCommitRoot(): void {
if (supportsTestSelectors) {
commitHooks.forEach(commitHook => commitHook());
}
}
十二、观察可见矩形
备注
setupIntersectionObserver()由宿主环境提供
export function observeVisibleRects(
hostRoot: Instance,
selectors: Array<Selector>,
callback: (
intersections: Array<{ ratio: number; rect: BoundingRect }>,
) => void,
options?: IntersectionObserverOptions,
): { disconnect: () => void } {
if (!supportsTestSelectors) {
throw new Error('Test selector API is not supported by this renderer.');
}
const instanceRoots = findAllNodes(hostRoot, selectors);
const { disconnect, observe, unobserve } = setupIntersectionObserver(
instanceRoots,
callback,
options,
);
// When React mutates the host environment, we may need to change what we're listening to.
// 当 React 更改宿主环境时,我们可能需要更改监听的内容。
const commitHook = () => {
const nextInstanceRoots = findAllNodes(hostRoot, selectors);
instanceRoots.forEach(target => {
if (nextInstanceRoots.indexOf(target) < 0) {
unobserve(target);
}
});
nextInstanceRoots.forEach(target => {
if (instanceRoots.indexOf(target) < 0) {
observe(target);
}
});
};
commitHooks.push(commitHook);
return {
disconnect: () => {
// Stop listening for React mutations:
// 停止监听 React 变更:
const index = commitHooks.indexOf(commitHook);
if (index >= 0) {
commitHooks.splice(index, 1);
}
// Disconnect the host observer:
// 断开宿主环境观察者:
disconnect();
},
};
}
十三、常量
备注
在源码中 545 行
// 提交钩子
const commitHooks: Array<Function> = [];
十四、变量
以二进制数据制定了类型,并判定平台是否支持 Symbol ,若支持,使用 Symbol 保证唯一。
let COMPONENT_TYPE: symbol | number = 0b000;
let HAS_PSEUDO_CLASS_TYPE: symbol | number = 0b001;
let ROLE_TYPE: symbol | number = 0b010;
let TEST_NAME_TYPE: symbol | number = 0b011;
let TEXT_TYPE: symbol | number = 0b100;
if (typeof Symbol === 'function' && Symbol.for) {
const symbolFor = Symbol.for;
COMPONENT_TYPE = symbolFor('selector.component');
HAS_PSEUDO_CLASS_TYPE = symbolFor('selector.has_pseudo_class');
ROLE_TYPE = symbolFor('selector.role');
TEST_NAME_TYPE = symbolFor('selector.test_id');
TEXT_TYPE = symbolFor('selector.text');
}
十五、工具
1. 查找宿主环境根的 Fiber 根
备注
getInstanceFromNode()由宿主环境提供findFiberRoot()由宿主环境提供
function findFiberRootForHostRoot(hostRoot: Instance): Fiber {
const maybeFiber = getInstanceFromNode(hostRoot as any);
if (maybeFiber != null) {
if (typeof maybeFiber.memoizedProps['data-testname'] !== 'string') {
throw new Error(
'Invalid host root specified. Should be either a React container or a node with a testname attribute.',
);
}
return maybeFiber as any as Fiber;
} else {
const fiberRoot = findFiberRoot(hostRoot);
if (fiberRoot === null) {
throw new Error(
'Could not find React container within specified host subtree.',
);
}
// The Flow type for FiberRoot is a little funky.
// createFiberRoot() cheats this by treating the root as :any and adding stateNode lazily.
//
// FiberRoot 的 Flow 类型有点特殊。
// createFiberRoot() 通过将根节点视为 :any 并延迟添加 stateNode 来绕过这个问题。
return (fiberRoot as any).stateNode.current as Fiber;
}
}
2. 匹配选择器
备注
getTextContent()由宿主环境提供
function matchSelector(fiber: Fiber, selector: Selector): boolean {
const tag = fiber.tag;
switch (selector.$$typeof) {
case COMPONENT_TYPE:
if (fiber.type === selector.value) {
return true;
}
break;
case HAS_PSEUDO_CLASS_TYPE:
return hasMatchingPaths(
fiber,
(selector as any as HasPseudoClassSelector).value,
);
case ROLE_TYPE:
if (
tag === HostComponent ||
tag === HostHoistable ||
tag === HostSingleton
) {
const node = fiber.stateNode;
if (
matchAccessibilityRole(node, (selector as any as RoleSelector).value)
) {
return true;
}
}
break;
case TEXT_TYPE:
if (
tag === HostComponent ||
tag === HostText ||
tag === HostHoistable ||
tag === HostSingleton
) {
const textContent = getTextContent(fiber);
if (
textContent !== null &&
textContent.indexOf((selector as any as TextSelector).value) >= 0
) {
return true;
}
}
break;
case TEST_NAME_TYPE:
if (
tag === HostComponent ||
tag === HostHoistable ||
tag === HostSingleton
) {
const dataTestID = fiber.memoizedProps['data-testname'];
if (
typeof dataTestID === 'string' &&
dataTestID.toLowerCase() ===
(selector as any as TestNameSelector).value.toLowerCase()
) {
return true;
}
}
break;
default:
throw new Error('Invalid selector type specified.');
}
return false;
}
3. 选择器转字符串
备注
getComponentNameFromType()由 shared 提供
function selectorToString(selector: Selector): string | null {
switch (selector.$$typeof) {
case COMPONENT_TYPE:
const displayName = getComponentNameFromType(selector.value) || 'Unknown';
return `<${displayName}>`;
case HAS_PSEUDO_CLASS_TYPE:
return `:has(${selectorToString(selector) || ''})`;
case ROLE_TYPE:
return `[role="${(selector as any as RoleSelector).value}"]`;
case TEXT_TYPE:
return `"${(selector as any as TextSelector).value}"`;
case TEST_NAME_TYPE:
return `[data-testname="${(selector as any as TestNameSelector).value}"]`;
default:
throw new Error('Invalid selector type specified.');
}
}
4. 查找路径
备注
isHiddenSubtree()由宿主环境提供
function findPaths(root: Fiber, selectors: Array<Selector>): Array<Fiber> {
const matchingFibers: Array<Fiber> = [];
const stack = [root, 0];
let index = 0;
while (index < stack.length) {
const fiber = stack[index++] as any as Fiber;
const tag = fiber.tag;
let selectorIndex = stack[index++] as any as number;
let selector = selectors[selectorIndex];
if (
(tag === HostComponent ||
tag === HostHoistable ||
tag === HostSingleton) &&
isHiddenSubtree(fiber)
) {
continue;
} else {
while (selector != null && matchSelector(fiber, selector)) {
selectorIndex++;
selector = selectors[selectorIndex];
}
}
if (selectorIndex === selectors.length) {
matchingFibers.push(fiber);
} else {
let child = fiber.child;
while (child !== null) {
stack.push(child, selectorIndex);
child = child.sibling;
}
}
}
return matchingFibers;
}
5. 有匹配路径
备注
isHiddenSubtree()由宿主环境提供
// Same as findPaths but with eager bailout on first match
// 与 findPaths 相同,但在首次匹配时立即退出
function hasMatchingPaths(root: Fiber, selectors: Array<Selector>): boolean {
const stack = [root, 0];
let index = 0;
while (index < stack.length) {
const fiber = stack[index++] as any as Fiber;
const tag = fiber.tag;
let selectorIndex = stack[index++] as any as number;
let selector = selectors[selectorIndex];
if (
(tag === HostComponent ||
tag === HostHoistable ||
tag === HostSingleton) &&
isHiddenSubtree(fiber)
) {
continue;
} else {
while (selector != null && matchSelector(fiber, selector)) {
selectorIndex++;
selector = selectors[selectorIndex];
}
}
if (selectorIndex === selectors.length) {
return true;
} else {
let child = fiber.child;
while (child !== null) {
stack.push(child, selectorIndex);
child = child.sibling;
}
}
}
return false;
}