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:
parent
439c8e9c40
commit
48cc41f3e7
|
|
@ -755,6 +755,7 @@ In react selectors, component names are transcribed with **CamelCase**.
|
||||||
Selector examples:
|
Selector examples:
|
||||||
|
|
||||||
- match by **component**: `_react=BookItem`
|
- 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 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 property value only, **case-insensitive**: `_react=[author = "steven king" i]`
|
||||||
- match by component and **truthy property value**: `_react=MyButton[enabled]`
|
- match by component and **truthy property value**: `_react=MyButton[enabled]`
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export type ParsedComponentAttribute = {
|
||||||
jsonPath: string[],
|
jsonPath: string[],
|
||||||
op: Operator,
|
op: Operator,
|
||||||
value: any,
|
value: any,
|
||||||
caseSensetive: boolean,
|
caseSensitive: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ParsedComponentSelector = {
|
export type ParsedComponentSelector = {
|
||||||
|
|
@ -32,8 +32,8 @@ export function checkComponentAttribute(obj: any, attr: ParsedComponentAttribute
|
||||||
if (obj !== undefined && obj !== null)
|
if (obj !== undefined && obj !== null)
|
||||||
obj = obj[token];
|
obj = obj[token];
|
||||||
}
|
}
|
||||||
const objValue = typeof obj === 'string' && !attr.caseSensetive ? obj.toUpperCase() : obj;
|
const objValue = typeof obj === 'string' && !attr.caseSensitive ? obj.toUpperCase() : obj;
|
||||||
const attrValue = typeof attr.value === 'string' && !attr.caseSensetive ? attr.value.toUpperCase() : attr.value;
|
const attrValue = typeof attr.value === 'string' && !attr.caseSensitive ? attr.value.toUpperCase() : attr.value;
|
||||||
|
|
||||||
if (attr.op === '<truthy>')
|
if (attr.op === '<truthy>')
|
||||||
return !!objValue;
|
return !!objValue;
|
||||||
|
|
@ -142,22 +142,22 @@ export function parseComponentSelector(selector: string): ParsedComponentSelecto
|
||||||
// check property is truthy: [enabled]
|
// check property is truthy: [enabled]
|
||||||
if (next() === ']') {
|
if (next() === ']') {
|
||||||
eat1();
|
eat1();
|
||||||
return { jsonPath, op: '<truthy>', value: null, caseSensetive: false };
|
return { jsonPath, op: '<truthy>', value: null, caseSensitive: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const operator = readOperator();
|
const operator = readOperator();
|
||||||
|
|
||||||
let value = undefined;
|
let value = undefined;
|
||||||
let caseSensetive = true;
|
let caseSensitive = true;
|
||||||
skipSpaces();
|
skipSpaces();
|
||||||
if (next() === `'` || next() === `"`) {
|
if (next() === `'` || next() === `"`) {
|
||||||
value = readQuotedString(next()).slice(1, -1);
|
value = readQuotedString(next()).slice(1, -1);
|
||||||
skipSpaces();
|
skipSpaces();
|
||||||
if (next() === 'i' || next() === 'I') {
|
if (next() === 'i' || next() === 'I') {
|
||||||
caseSensetive = false;
|
caseSensitive = false;
|
||||||
eat1();
|
eat1();
|
||||||
} else if (next() === 's' || next() === 'S') {
|
} else if (next() === 's' || next() === 'S') {
|
||||||
caseSensetive = true;
|
caseSensitive = true;
|
||||||
eat1();
|
eat1();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -181,7 +181,7 @@ export function parseComponentSelector(selector: string): ParsedComponentSelecto
|
||||||
eat1();
|
eat1();
|
||||||
if (operator !== '=' && typeof value !== 'string')
|
if (operator !== '=' && typeof value !== 'string')
|
||||||
throw new Error(`Error while parsing selector \`${selector}\` - cannot use ${operator} in attribute with non-string matching value - ${value}`);
|
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 = {
|
const result: ParsedComponentSelector = {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { isInsideScope } from './selectorEvaluator';
|
||||||
import { checkComponentAttribute, parseComponentSelector } from './componentUtils';
|
import { checkComponentAttribute, parseComponentSelector } from './componentUtils';
|
||||||
|
|
||||||
type ComponentNode = {
|
type ComponentNode = {
|
||||||
|
key?: any,
|
||||||
name: string,
|
name: string,
|
||||||
children: ComponentNode[],
|
children: ComponentNode[],
|
||||||
rootElements: Element[],
|
rootElements: Element[],
|
||||||
|
|
@ -26,6 +27,7 @@ type ComponentNode = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type ReactVNode = {
|
type ReactVNode = {
|
||||||
|
key?: any,
|
||||||
// React 16+
|
// React 16+
|
||||||
type: any,
|
type: any,
|
||||||
child?: ReactVNode,
|
child?: ReactVNode,
|
||||||
|
|
@ -60,6 +62,10 @@ function getComponentName(reactElement: ReactVNode): string {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getComponentKey(reactElement: ReactVNode): any {
|
||||||
|
return reactElement.key ?? reactElement._currentElement?.key;
|
||||||
|
}
|
||||||
|
|
||||||
function getChildren(reactElement: ReactVNode): ReactVNode[] {
|
function getChildren(reactElement: ReactVNode): ReactVNode[] {
|
||||||
// React 16+
|
// React 16+
|
||||||
// @see https://github.com/baruchvlz/resq/blob/5c15a5e04d3f7174087248f5a158c3d6dcc1ec72/src/utils.js#L192
|
// @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 {
|
function buildComponentsTree(reactElement: ReactVNode): ComponentNode {
|
||||||
const treeNode: ComponentNode = {
|
const treeNode: ComponentNode = {
|
||||||
|
key: getComponentKey(reactElement),
|
||||||
name: getComponentName(reactElement),
|
name: getComponentName(reactElement),
|
||||||
children: getChildren(reactElement).map(buildComponentsTree),
|
children: getChildren(reactElement).map(buildComponentsTree),
|
||||||
rootElements: [],
|
rootElements: [],
|
||||||
|
|
@ -165,12 +172,17 @@ export const ReactEngine: SelectorEngine = {
|
||||||
const reactRoots = findReactRoots(document);
|
const reactRoots = findReactRoots(document);
|
||||||
const trees = reactRoots.map(reactRoot => buildComponentsTree(reactRoot));
|
const trees = reactRoots.map(reactRoot => buildComponentsTree(reactRoot));
|
||||||
const treeNodes = trees.map(tree => filterComponentsTree(tree, treeNode => {
|
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)
|
if (name && treeNode.name !== name)
|
||||||
return false;
|
return false;
|
||||||
if (treeNode.rootElements.some(domNode => !isInsideScope(scope, domNode)))
|
if (treeNode.rootElements.some(domNode => !isInsideScope(scope, domNode)))
|
||||||
return false;
|
return false;
|
||||||
for (const attr of attributes) {
|
for (const attr of attributes) {
|
||||||
if (!checkComponentAttribute(treeNode.props, attr))
|
if (!checkComponentAttribute(props, attr))
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -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('.');
|
const path = attr.jsonPath.map(token => /^[a-zA-Z0-9]+$/i.test(token) ? token : JSON.stringify(token)).join('.');
|
||||||
if (attr.op === '<truthy>')
|
if (attr.op === '<truthy>')
|
||||||
return '[' + path + ']';
|
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('');
|
}).join('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ for (const [name, url] of Object.entries(reacts)) {
|
||||||
it('should query by props combinations', async ({ page }) => {
|
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"]`, 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=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.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.nonexisting.index = 0]`, els => els.length)).toBe(0);
|
||||||
expect(await page.$$eval(`_react=ColorButton[nested.index.nonexisting = 0]`, els => els.length)).toBe(0);
|
expect(await page.$$eval(`_react=ColorButton[nested.index.nonexisting = 0]`, els => els.length)).toBe(0);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue