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:
parent
38c89df330
commit
7e6e5f0706
|
|
@ -43,13 +43,23 @@ type ReactVNode = {
|
||||||
_renderedChildren?: any[],
|
_renderedChildren?: any[],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getFunctionComponentName(component: any) {
|
||||||
|
return component.displayName || component.name || 'Anonymous';
|
||||||
|
}
|
||||||
|
|
||||||
function getComponentName(reactElement: ReactVNode): string {
|
function getComponentName(reactElement: ReactVNode): string {
|
||||||
// React 16+
|
// React 16+
|
||||||
// @see https://github.com/baruchvlz/resq/blob/5c15a5e04d3f7174087248f5a158c3d6dcc1ec72/src/utils.js#L16
|
// @see https://github.com/baruchvlz/resq/blob/5c15a5e04d3f7174087248f5a158c3d6dcc1ec72/src/utils.js#L16
|
||||||
if (typeof reactElement.type === 'function')
|
if (reactElement.type) {
|
||||||
return reactElement.type.displayName || reactElement.type.name || 'Anonymous';
|
switch (typeof reactElement.type) {
|
||||||
if (typeof reactElement.type === 'string')
|
case 'function':
|
||||||
|
return getFunctionComponentName(reactElement.type);
|
||||||
|
case 'string':
|
||||||
return reactElement.type;
|
return reactElement.type;
|
||||||
|
case 'object': // support memo and forwardRef
|
||||||
|
return reactElement.type.displayName || (reactElement.type.render ? getFunctionComponentName(reactElement.type.render) : '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// React 15
|
// React 15
|
||||||
// @see https://github.com/facebook/react/blob/2edf449803378b5c58168727d4f123de3ba5d37f/packages/react-devtools-shared/src/backend/legacy/renderer.js#L59
|
// @see https://github.com/facebook/react/blob/2edf449803378b5c58168727d4f123de3ba5d37f/packages/react-devtools-shared/src/backend/legacy/renderer.js#L59
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ function ColorButton (props) {
|
||||||
return e('button', {className: props.color, disabled: !props.enabled}, 'button ' + props.nested.index);
|
return e('button', {className: props.color, disabled: !props.enabled}, 'button ' + props.nested.index);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ButtonGrid() {
|
const ButtonGrid = React.memo(function() {
|
||||||
const buttons = [];
|
const buttons = [];
|
||||||
for (let i = 0; i < 9; ++i) {
|
for (let i = 0; i < 9; ++i) {
|
||||||
buttons.push(e(ColorButton, {
|
buttons.push(e(ColorButton, {
|
||||||
|
|
@ -51,7 +51,9 @@ function ButtonGrid() {
|
||||||
}, null));
|
}, null));
|
||||||
};
|
};
|
||||||
return e(React.Fragment, null, ...buttons);
|
return e(React.Fragment, null, ...buttons);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
ButtonGrid.displayName = "ButtonGrid";
|
||||||
|
|
||||||
class BookItem extends React.Component {
|
class BookItem extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ function ColorButton (props) {
|
||||||
return e('button', {className: props.color, disabled: !props.enabled}, 'button ' + props.nested.index);
|
return e('button', {className: props.color, disabled: !props.enabled}, 'button ' + props.nested.index);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ButtonGrid() {
|
const ButtonGrid = React.memo(function() {
|
||||||
const buttons = [];
|
const buttons = [];
|
||||||
for (let i = 0; i < 9; ++i) {
|
for (let i = 0; i < 9; ++i) {
|
||||||
buttons.push(e(ColorButton, {
|
buttons.push(e(ColorButton, {
|
||||||
|
|
@ -51,7 +51,9 @@ function ButtonGrid() {
|
||||||
}, null));
|
}, null));
|
||||||
};
|
};
|
||||||
return e(React.Fragment, null, ...buttons);
|
return e(React.Fragment, null, ...buttons);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
ButtonGrid.displayName = "ButtonGrid";
|
||||||
|
|
||||||
class BookItem extends React.Component {
|
class BookItem extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,11 @@ for (const [name, url] of Object.entries(reacts)) {
|
||||||
await expect(page.locator(`_react=BookItem`)).toHaveCount(6);
|
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 }) => {
|
it('should work with multiroot react', async ({ page }) => {
|
||||||
await it.step('mount second root', async () => {
|
await it.step('mount second root', async () => {
|
||||||
await expect(page.locator(`_react=BookItem`)).toHaveCount(3);
|
await expect(page.locator(`_react=BookItem`)).toHaveCount(3);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue