feat: add key support on react engine (#11970)

I've got [this question](https://stackoverflow.com/questions/71050193/react-locator-example/71052432#71052432) on StackOverflow. And although, in that case, the `key` was part of the `props` attributes. That might not always be true.

I am bringing this to the tell to see what you think about this.
I'm also fixing a typo :)
This commit is contained in:
Darío Kondratiuk 2022-02-09 16:33:15 -03:00 committed by GitHub
parent 439c8e9c40
commit 48cc41f3e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 24 additions and 10 deletions

View File

@ -755,6 +755,7 @@ In react selectors, component names are transcribed with **CamelCase**.
Selector examples:
- match by **component**: `_react=BookItem`
- match by component and **key**: `_react=BookItem[key = '2']`
- match by component and **exact property value**, case-sensitive: `_react=BookItem[author = "Steven King"]`
- match by property value only, **case-insensitive**: `_react=[author = "steven king" i]`
- match by component and **truthy property value**: `_react=MyButton[enabled]`

View File

@ -19,7 +19,7 @@ export type ParsedComponentAttribute = {
jsonPath: string[],
op: Operator,
value: any,
caseSensetive: boolean,
caseSensitive: boolean,
};
export type ParsedComponentSelector = {
@ -32,8 +32,8 @@ export function checkComponentAttribute(obj: any, attr: ParsedComponentAttribute
if (obj !== undefined && obj !== null)
obj = obj[token];
}
const objValue = typeof obj === 'string' && !attr.caseSensetive ? obj.toUpperCase() : obj;
const attrValue = typeof attr.value === 'string' && !attr.caseSensetive ? attr.value.toUpperCase() : attr.value;
const objValue = typeof obj === 'string' && !attr.caseSensitive ? obj.toUpperCase() : obj;
const attrValue = typeof attr.value === 'string' && !attr.caseSensitive ? attr.value.toUpperCase() : attr.value;
if (attr.op === '<truthy>')
return !!objValue;
@ -142,22 +142,22 @@ export function parseComponentSelector(selector: string): ParsedComponentSelecto
// check property is truthy: [enabled]
if (next() === ']') {
eat1();
return { jsonPath, op: '<truthy>', value: null, caseSensetive: false };
return { jsonPath, op: '<truthy>', value: null, caseSensitive: false };
}
const operator = readOperator();
let value = undefined;
let caseSensetive = true;
let caseSensitive = true;
skipSpaces();
if (next() === `'` || next() === `"`) {
value = readQuotedString(next()).slice(1, -1);
skipSpaces();
if (next() === 'i' || next() === 'I') {
caseSensetive = false;
caseSensitive = false;
eat1();
} else if (next() === 's' || next() === 'S') {
caseSensetive = true;
caseSensitive = true;
eat1();
}
} else {
@ -181,7 +181,7 @@ export function parseComponentSelector(selector: string): ParsedComponentSelecto
eat1();
if (operator !== '=' && typeof value !== 'string')
throw new Error(`Error while parsing selector \`${selector}\` - cannot use ${operator} in attribute with non-string matching value - ${value}`);
return { jsonPath, op: operator, value, caseSensetive };
return { jsonPath, op: operator, value, caseSensitive };
}
const result: ParsedComponentSelector = {

View File

@ -19,6 +19,7 @@ import { isInsideScope } from './selectorEvaluator';
import { checkComponentAttribute, parseComponentSelector } from './componentUtils';
type ComponentNode = {
key?: any,
name: string,
children: ComponentNode[],
rootElements: Element[],
@ -26,6 +27,7 @@ type ComponentNode = {
};
type ReactVNode = {
key?: any,
// React 16+
type: any,
child?: ReactVNode,
@ -60,6 +62,10 @@ function getComponentName(reactElement: ReactVNode): string {
return '';
}
function getComponentKey(reactElement: ReactVNode): any {
return reactElement.key ?? reactElement._currentElement?.key;
}
function getChildren(reactElement: ReactVNode): ReactVNode[] {
// React 16+
// @see https://github.com/baruchvlz/resq/blob/5c15a5e04d3f7174087248f5a158c3d6dcc1ec72/src/utils.js#L192
@ -104,6 +110,7 @@ function getProps(reactElement: ReactVNode) {
function buildComponentsTree(reactElement: ReactVNode): ComponentNode {
const treeNode: ComponentNode = {
key: getComponentKey(reactElement),
name: getComponentName(reactElement),
children: getChildren(reactElement).map(buildComponentsTree),
rootElements: [],
@ -165,12 +172,17 @@ export const ReactEngine: SelectorEngine = {
const reactRoots = findReactRoots(document);
const trees = reactRoots.map(reactRoot => buildComponentsTree(reactRoot));
const treeNodes = trees.map(tree => filterComponentsTree(tree, treeNode => {
const props = treeNode.props ?? {};
if (treeNode.key !== undefined)
props.key = treeNode.key;
if (name && treeNode.name !== name)
return false;
if (treeNode.rootElements.some(domNode => !isInsideScope(scope, domNode)))
return false;
for (const attr of attributes) {
if (!checkComponentAttribute(treeNode.props, attr))
if (!checkComponentAttribute(props, attr))
return false;
}
return true;

View File

@ -23,7 +23,7 @@ const serialize = (parsed: ParsedComponentSelector) => {
const path = attr.jsonPath.map(token => /^[a-zA-Z0-9]+$/i.test(token) ? token : JSON.stringify(token)).join('.');
if (attr.op === '<truthy>')
return '[' + path + ']';
return '[' + path + ' ' + attr.op + ' ' + JSON.stringify(attr.value) + (attr.caseSensetive ? ']' : ' i]');
return '[' + path + ' ' + attr.op + ' ' + JSON.stringify(attr.value) + (attr.caseSensitive ? ']' : ' i]');
}).join('');
};

View File

@ -58,6 +58,7 @@ for (const [name, url] of Object.entries(reacts)) {
it('should query by props combinations', async ({ page }) => {
expect(await page.$$eval(`_react=BookItem[name="The Great Gatsby"]`, els => els.length)).toBe(1);
expect(await page.$$eval(`_react=BookItem[name="the great gatsby" i]`, els => els.length)).toBe(1);
expect(await page.$$eval(`_react=li[key="The Great Gatsby"]`, els => els.length)).toBe(1);
expect(await page.$$eval(`_react=ColorButton[nested.index = 0]`, els => els.length)).toBe(1);
expect(await page.$$eval(`_react=ColorButton[nested.nonexisting.index = 0]`, els => els.length)).toBe(0);
expect(await page.$$eval(`_react=ColorButton[nested.index.nonexisting = 0]`, els => els.length)).toBe(0);