diff --git a/packages/playwright-core/src/server/injected/reactSelectorEngine.ts b/packages/playwright-core/src/server/injected/reactSelectorEngine.ts index 30bf0f9a10..1fad96b0c5 100644 --- a/packages/playwright-core/src/server/injected/reactSelectorEngine.ts +++ b/packages/playwright-core/src/server/injected/reactSelectorEngine.ts @@ -147,16 +147,20 @@ function findReactRoots(root: Document | ShadowRoot, roots: ReactVNode[] = []): do { const node = walker.currentNode; + const reactNode = node as { readonly [customKey: string]: any }; // React 17+ // React sets rootKey when mounting // @see https://github.com/facebook/react/blob/a724a3b578dce77d427bef313102a4d0e978d9b4/packages/react-dom/src/client/ReactDOMComponentTree.js#L62-L64 - const rootKey = Object.keys(node).find(key => key.startsWith('__reactContainer')); + const rootKey = Object.keys(reactNode).find(key => key.startsWith('__reactContainer') && reactNode[key] !== null); if (rootKey) { - roots.push((node as any)[rootKey].stateNode.current); - } else if (node.hasOwnProperty('_reactRootContainer')) { - // ReactDOM Legacy client API: - // @see https://github.com/baruchvlz/resq/blob/5c15a5e04d3f7174087248f5a158c3d6dcc1ec72/src/utils.js#L329 - roots.push((node as any)._reactRootContainer._internalRoot.current); + roots.push(reactNode[rootKey].stateNode.current); + } else { + const legacyRootKey = '_reactRootContainer'; + if (reactNode.hasOwnProperty(legacyRootKey) && reactNode[legacyRootKey] !== null) { + // ReactDOM Legacy client API: + // @see https://github.com/baruchvlz/resq/blob/5c15a5e04d3f7174087248f5a158c3d6dcc1ec72/src/utils.js#L329 + roots.push(reactNode[legacyRootKey]._internalRoot.current); + } } // Pre-react 16: rely on `data-reactroot` diff --git a/tests/assets/reading-list/react15.html b/tests/assets/reading-list/react15.html index b9728f8560..2ae222e569 100644 --- a/tests/assets/reading-list/react15.html +++ b/tests/assets/reading-list/react15.html @@ -102,8 +102,12 @@ class App extends React.Component { } } -window.mountApp = element => ReactDOM.render(e(App, null, null), element); +window.mountApp = element => { + const root = ReactDOM.render(e(App, null, null), element); + const unmount = () => ReactDOM.unmountComponentAtNode(element); + return {root, unmount}; +}; window.app = window.mountApp(document.getElementById('root')); -window.mountNestedApp = () => window.mountApp(window.app.refs.mountPoint); +window.mountNestedApp = () => window.mountApp(window.app.root.refs.mountPoint); diff --git a/tests/assets/reading-list/react16.html b/tests/assets/reading-list/react16.html index 184204f5d2..f77867dfd7 100644 --- a/tests/assets/reading-list/react16.html +++ b/tests/assets/reading-list/react16.html @@ -50,7 +50,7 @@ class NewBook extends React.Component { } render() { - return e(React.Fragment, null, + return e(React.Fragment, null, e('input', {onInput: this.onInput.bind(this)}, null), e('button', { onClick: () => this.props.onNewBook(this.state), @@ -101,9 +101,13 @@ class App extends React.Component { } } -window.mountApp = element => ReactDOM.render(e(App, null, null), element); +window.mountApp = element => { + const root = ReactDOM.render(e(App, null, null), element); + const unmount = () => ReactDOM.unmountComponentAtNode(element); + return {root, unmount}; +}; window.app = window.mountApp(document.getElementById('root')); -window.mountNestedApp = () => window.mountApp(window.app.mountPoint.current); +window.mountNestedApp = () => window.mountApp(window.app.root.mountPoint.current); diff --git a/tests/assets/reading-list/react17.html b/tests/assets/reading-list/react17.html index 2eb9e7cc1d..e9a8d7eeaf 100644 --- a/tests/assets/reading-list/react17.html +++ b/tests/assets/reading-list/react17.html @@ -95,8 +95,12 @@ class App extends React.Component { } } -window.mountApp = element => ReactDOM.render(e(App, null, null), element); +window.mountApp = element => { + const root = ReactDOM.render(e(App, null, null), element); + const unmount = () => ReactDOM.unmountComponentAtNode(element); + return {root, unmount}; +}; window.app = window.mountApp(document.getElementById('root')); -window.mountNestedApp = () => window.mountApp(window.app.mountPoint.current); +window.mountNestedApp = () => window.mountApp(window.app.root.mountPoint.current); diff --git a/tests/page/selectors-react.spec.ts b/tests/page/selectors-react.spec.ts index bd3234b95a..9ad71532aa 100644 --- a/tests/page/selectors-react.spec.ts +++ b/tests/page/selectors-react.spec.ts @@ -156,6 +156,19 @@ for (const [name, url] of Object.entries(reacts)) { }); await expect(page.locator(`_react=BookItem`)).toHaveCount(6); }); + + it('should work with multiroot react after unmount', async ({ page }) => { + await expect(page.locator(`_react=BookItem`)).toHaveCount(3); + + await page.evaluate(() => { + const anotherRoot = document.createElement('div'); + document.body.append(anotherRoot); + // @ts-ignore + const newRoot = window.mountApp(anotherRoot); + newRoot.unmount(); + }); + await expect(page.locator(`_react=BookItem`)).toHaveCount(3); + }); }); }