跳到主要内容

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;
}