mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-05 19:04:43 +03:00
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:
|
||||
|
||||
- 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]`
|
||||
|
@ -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 = {
|
||||
|
@ -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;
|
||||
|
@ -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('');
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user