feat: Support React forwards refs and memo (#23262)

This PR fixes the react selector behavior to support components that are
wrapped by the memo or forwardRef React builtin functions.

Previously these components couldn't be selected. This PR fixes that
behavior, enabling selecting those components.

Current behavior:
```
const Foo = memo(() => <div id="foo_component" />);
Foo.displayName = "Foo";
...
playwright.$("_react=Foo") -> undefined
```

Fixed behavior:
```
const Foo = memo(() => <div id="foo_component" />);
Foo.displayName = "Foo";
...
playwright.$("_react=Foo") -> <div id ="foo_component" />
```
This commit is contained in:
João Neves 2023-05-31 01:14:47 +01:00 committed by GitHub
parent 38c89df330
commit 7e6e5f0706
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 27 additions and 8 deletions

View File

@ -43,13 +43,23 @@ type ReactVNode = {
_renderedChildren?: any[],
};
function getFunctionComponentName(component: any) {
return component.displayName || component.name || 'Anonymous';
}
function getComponentName(reactElement: ReactVNode): string {
// React 16+
// @see https://github.com/baruchvlz/resq/blob/5c15a5e04d3f7174087248f5a158c3d6dcc1ec72/src/utils.js#L16
if (typeof reactElement.type === 'function')
return reactElement.type.displayName || reactElement.type.name || 'Anonymous';
if (typeof reactElement.type === 'string')
return reactElement.type;
if (reactElement.type) {
switch (typeof reactElement.type) {
case 'function':
return getFunctionComponentName(reactElement.type);
case 'string':
return reactElement.type;
case 'object': // support memo and forwardRef
return reactElement.type.displayName || (reactElement.type.render ? getFunctionComponentName(reactElement.type.render) : '');
}
}
// React 15
// @see https://github.com/facebook/react/blob/2edf449803378b5c58168727d4f123de3ba5d37f/packages/react-devtools-shared/src/backend/legacy/renderer.js#L59

View File

@ -38,7 +38,7 @@ function ColorButton (props) {
return e('button', {className: props.color, disabled: !props.enabled}, 'button ' + props.nested.index);
}
function ButtonGrid() {
const ButtonGrid = React.memo(function() {
const buttons = [];
for (let i = 0; i < 9; ++i) {
buttons.push(e(ColorButton, {
@ -51,7 +51,9 @@ function ButtonGrid() {
}, null));
};
return e(React.Fragment, null, ...buttons);
}
});
ButtonGrid.displayName = "ButtonGrid";
class BookItem extends React.Component {
render() {

View File

@ -38,7 +38,7 @@ function ColorButton (props) {
return e('button', {className: props.color, disabled: !props.enabled}, 'button ' + props.nested.index);
}
function ButtonGrid() {
const ButtonGrid = React.memo(function() {
const buttons = [];
for (let i = 0; i < 9; ++i) {
buttons.push(e(ColorButton, {
@ -51,7 +51,9 @@ function ButtonGrid() {
}, null));
};
return e(React.Fragment, null, ...buttons);
}
});
ButtonGrid.displayName = "ButtonGrid";
class BookItem extends React.Component {
render() {

View File

@ -124,6 +124,11 @@ for (const [name, url] of Object.entries(reacts)) {
await expect(page.locator(`_react=BookItem`)).toHaveCount(6);
});
it('should work with react memo', async ({ page }) => {
it.skip(name === 'react15' || name === 'react16', 'Class components dont support memo');
await expect(page.locator(`_react=ButtonGrid`)).toHaveCount(9);
});
it('should work with multiroot react', async ({ page }) => {
await it.step('mount second root', async () => {
await expect(page.locator(`_react=BookItem`)).toHaveCount(3);