Use ReactDOM Test Selector API in DevTools e2e tests (#22978)

Builds on top of the existing Playwright tests to plug in the test selector API: https://gist.github.com/bvaughn/d3c8b8842faf2ac2439bb11773a19cec

My goals in doing this are to...
1. Experiment with the new API to see what works and what doesn't.
2. Add some test selector attributes (and remove DOM-structure based selectors).
3. Focus the tests on DevTools itself (rather than the test app).

I also took this opportunity to add a few new test cases– like named hooks, editable props, component search, and profiling- just to play around more with the Playwright API.

Relates to issue #22646
This commit is contained in:
Brian Vaughn
2021-12-21 11:58:04 -05:00
committed by GitHub
parent ceee524a8f
commit a4ead704ba
24 changed files with 549 additions and 74 deletions

View File

@@ -0,0 +1,206 @@
/** @flow */
'use strict';
const listAppUtils = require('./list-app-utils');
const devToolsUtils = require('./devtools-utils');
const {test, expect} = require('@playwright/test');
const config = require('../../playwright.config');
test.use(config);
test.describe('Components', () => {
let page;
test.beforeEach(async ({browser}) => {
page = await browser.newPage();
await page.goto('http://localhost:8080/e2e.html', {
waitUntil: 'domcontentloaded',
});
await page.waitForSelector('#iframe');
await devToolsUtils.clickButton(page, 'TabBarButton-components');
});
test('Should display initial React components', async () => {
const appRowCount = await page.evaluate(() => {
const {createTestNameSelector, findAllNodes} = window.REACT_DOM_APP;
const container = document.getElementById('iframe').contentDocument;
const rows = findAllNodes(container, [
createTestNameSelector('ListItem'),
]);
return rows.length;
});
expect(appRowCount).toBe(3);
const devToolsRowCount = await devToolsUtils.getElementCount(
page,
'ListItem'
);
expect(devToolsRowCount).toBe(3);
});
test('Should display newly added React components', async () => {
await listAppUtils.addItem(page, 'four');
const count = await devToolsUtils.getElementCount(page, 'ListItem');
expect(count).toBe(4);
});
test('Should allow elements to be inspected', async () => {
// Select the first list item in DevTools.
await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp');
// Then read the inspected values.
const [propName, propValue, sourceText] = await page.evaluate(() => {
const {createTestNameSelector, findAllNodes} = window.REACT_DOM_DEVTOOLS;
const container = document.getElementById('devtools');
const editableName = findAllNodes(container, [
createTestNameSelector('InspectedElementPropsTree'),
createTestNameSelector('EditableName'),
])[0];
const editableValue = findAllNodes(container, [
createTestNameSelector('InspectedElementPropsTree'),
createTestNameSelector('EditableValue'),
])[0];
const source = findAllNodes(container, [
createTestNameSelector('InspectedElementView-Source'),
])[0];
return [editableName.value, editableValue.value, source.innerText];
});
expect(propName).toBe('label');
expect(propValue).toBe('"one"');
expect(sourceText).toContain('ListApp.js');
});
test('should allow props to be edited', async () => {
// Select the first list item in DevTools.
await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp');
// Then edit the label prop.
await page.evaluate(() => {
const {createTestNameSelector, focusWithin} = window.REACT_DOM_DEVTOOLS;
const container = document.getElementById('devtools');
focusWithin(container, [
createTestNameSelector('InspectedElementPropsTree'),
createTestNameSelector('EditableValue'),
]);
});
page.keyboard.press('Backspace'); // "
page.keyboard.press('Backspace'); // e
page.keyboard.press('Backspace'); // n
page.keyboard.press('Backspace'); // o
page.keyboard.insertText('new"');
page.keyboard.press('Enter');
await page.waitForFunction(() => {
const {createTestNameSelector, findAllNodes} = window.REACT_DOM_APP;
const container = document.getElementById('iframe').contentDocument;
const rows = findAllNodes(container, [
createTestNameSelector('ListItem'),
])[0];
return rows.innerText === 'new';
});
});
test('should load and parse hook names for the inspected element', async () => {
// Select the List component DevTools.
await devToolsUtils.selectElement(page, 'List', 'App');
// Then click to load and parse hook names.
await devToolsUtils.clickButton(page, 'LoadHookNamesButton');
// Make sure the expected hook names are parsed and displayed eventually.
await page.waitForFunction(
hookNames => {
const {
createTestNameSelector,
findAllNodes,
} = window.REACT_DOM_DEVTOOLS;
const container = document.getElementById('devtools');
const hooksTree = findAllNodes(container, [
createTestNameSelector('InspectedElementHooksTree'),
])[0];
if (!hooksTree) {
return false;
}
const hooksTreeText = hooksTree.innerText;
for (let i = 0; i < hookNames.length; i++) {
if (!hooksTreeText.includes(hookNames[i])) {
return false;
}
}
return true;
},
['State(items)', 'Ref(inputRef)']
);
});
test('should allow searching for component by name', async () => {
async function getComponentSearchResultsCount() {
return await page.evaluate(() => {
const {
createTestNameSelector,
findAllNodes,
} = window.REACT_DOM_DEVTOOLS;
const container = document.getElementById('devtools');
const element = findAllNodes(container, [
createTestNameSelector('ComponentSearchInput-ResultsCount'),
])[0];
return element.innerText;
});
}
await page.evaluate(() => {
const {createTestNameSelector, focusWithin} = window.REACT_DOM_DEVTOOLS;
const container = document.getElementById('devtools');
focusWithin(container, [
createTestNameSelector('ComponentSearchInput-Input'),
]);
});
page.keyboard.insertText('List');
let count = await getComponentSearchResultsCount();
expect(count).toBe('1 | 4');
page.keyboard.insertText('Item');
count = await getComponentSearchResultsCount();
expect(count).toBe('1 | 3');
page.keyboard.press('Enter');
count = await getComponentSearchResultsCount();
expect(count).toBe('2 | 3');
page.keyboard.press('Enter');
count = await getComponentSearchResultsCount();
expect(count).toBe('3 | 3');
page.keyboard.press('Enter');
count = await getComponentSearchResultsCount();
expect(count).toBe('1 | 3');
page.keyboard.press('Shift+Enter');
count = await getComponentSearchResultsCount();
expect(count).toBe('3 | 3');
page.keyboard.press('Shift+Enter');
count = await getComponentSearchResultsCount();
expect(count).toBe('2 | 3');
page.keyboard.press('Shift+Enter');
count = await getComponentSearchResultsCount();
expect(count).toBe('1 | 3');
});
});

View File

@@ -0,0 +1,83 @@
'use strict';
/** @flow */
async function clickButton(page, buttonTestName) {
await page.evaluate(testName => {
const {createTestNameSelector, findAllNodes} = window.REACT_DOM_DEVTOOLS;
const container = document.getElementById('devtools');
const button = findAllNodes(container, [
createTestNameSelector(testName),
])[0];
button.click();
}, buttonTestName);
}
async function getElementCount(page, displayName) {
return await page.evaluate(listItemText => {
const {
createTestNameSelector,
createTextSelector,
findAllNodes,
} = window.REACT_DOM_DEVTOOLS;
const container = document.getElementById('devtools');
const rows = findAllNodes(container, [
createTestNameSelector('ComponentTreeListItem'),
createTextSelector(listItemText),
]);
return rows.length;
}, displayName);
}
async function selectElement(page, displayName, waitForOwnersText) {
await page.evaluate(listItemText => {
const {
createTestNameSelector,
createTextSelector,
findAllNodes,
} = window.REACT_DOM_DEVTOOLS;
const container = document.getElementById('devtools');
const listItem = findAllNodes(container, [
createTestNameSelector('ComponentTreeListItem'),
createTextSelector(listItemText),
])[0];
listItem.click();
}, displayName);
if (waitForOwnersText) {
// Wait for selected element's props to load.
await page.waitForFunction(
({titleText, ownersListText}) => {
const {
createTestNameSelector,
findAllNodes,
} = window.REACT_DOM_DEVTOOLS;
const container = document.getElementById('devtools');
const title = findAllNodes(container, [
createTestNameSelector('InspectedElement-Title'),
])[0];
const ownersList = findAllNodes(container, [
createTestNameSelector('InspectedElementView-Owners'),
])[0];
return (
title &&
title.innerText.includes(titleText) &&
ownersList &&
ownersList.innerText.includes(ownersListText)
);
},
{titleText: displayName, ownersListText: waitForOwnersText}
);
}
}
module.exports = {
clickButton,
getElementCount,
selectElement,
};

View File

@@ -1,52 +0,0 @@
'use strict';
const {test, expect} = require('@playwright/test');
const config = require('../../playwright.config');
test.use(config);
test.describe('Testing Todo-List App', () => {
let page, frameElementHandle, frame;
test.beforeAll(async ({browser}) => {
page = await browser.newPage();
await page.goto('http://localhost:8080/e2e.html', {
waitUntil: 'domcontentloaded',
});
await page.waitForSelector('iframe#iframe');
frameElementHandle = await page.$('#iframe');
frame = await frameElementHandle.contentFrame();
});
test('The Todo List should contain 3 items by default', async () => {
const list = frame.locator('.listitem');
await expect(list).toHaveCount(3);
});
test('Add another item Fourth to list', async () => {
await frame.type('.input', 'Fourth');
await frame.click('button.iconbutton');
const listItems = await frame.locator('.label');
await expect(listItems).toHaveText(['First', 'Second', 'Third', 'Fourth']);
});
test('Inspecting list elements with devtools', async () => {
// Component props are used as string in devtools.
const listItemsProps = [
'',
'{id: 1, isComplete: true, text: "First"}',
'{id: 2, isComplete: true, text: "Second"}',
'{id: 3, isComplete: false, text: "Third"}',
'{id: 4, isComplete: false, text: "Fourth"}',
];
const countOfItems = await frame.$$eval('.listitem', el => el.length);
// For every item in list click on devtools inspect icon
// click on the list item to quickly navigate to the list item component in devtools
// comparing displayed props with the array of props.
for (let i = 1; i <= countOfItems; ++i) {
await page.click('[class^=ToggleContent]', {delay: 100});
await frame.click(`.listitem:nth-child(${i})`, {delay: 50});
await page.waitForSelector('span[class^=Value]');
const text = await page.innerText('span[class^=Value]');
await expect(text).toEqual(listItemsProps[i]);
}
});
});

View File

@@ -0,0 +1,25 @@
'use strict';
/** @flow */
async function addItem(page, newItemText) {
await page.evaluate(text => {
const {createTestNameSelector, findAllNodes} = window.REACT_DOM_APP;
const container = document.getElementById('iframe').contentDocument;
const input = findAllNodes(container, [
createTestNameSelector('AddItemInput'),
])[0];
input.value = text;
const button = findAllNodes(container, [
createTestNameSelector('AddItemButton'),
])[0];
button.click();
}, newItemText);
}
module.exports = {
addItem,
};

View File

@@ -0,0 +1,104 @@
/** @flow */
'use strict';
const listAppUtils = require('./list-app-utils');
const devToolsUtils = require('./devtools-utils');
const {test, expect} = require('@playwright/test');
const config = require('../../playwright.config');
test.use(config);
test.describe('Profiler', () => {
let page;
test.beforeEach(async ({browser}) => {
page = await browser.newPage();
await page.goto('http://localhost:8080/e2e.html', {
waitUntil: 'domcontentloaded',
});
await page.waitForSelector('#iframe');
await devToolsUtils.clickButton(page, 'TabBarButton-profiler');
});
test('should record renders and commits when active', async () => {
async function getSnapshotSelectorText() {
return await page.evaluate(() => {
const {
createTestNameSelector,
findAllNodes,
} = window.REACT_DOM_DEVTOOLS;
const container = document.getElementById('devtools');
const input = findAllNodes(container, [
createTestNameSelector('SnapshotSelector-Input'),
])[0];
const label = findAllNodes(container, [
createTestNameSelector('SnapshotSelector-Label'),
])[0];
return `${input.value}${label.innerText}`;
});
}
async function clickButtonAndVerifySnapshotSelecetorText(
buttonTagName,
expectedText
) {
await devToolsUtils.clickButton(page, buttonTagName);
const text = await getSnapshotSelectorText();
expect(text).toBe(expectedText);
}
await devToolsUtils.clickButton(page, 'ProfilerToggleButton');
await listAppUtils.addItem(page, 'four');
await listAppUtils.addItem(page, 'five');
await listAppUtils.addItem(page, 'six');
await devToolsUtils.clickButton(page, 'ProfilerToggleButton');
await page.waitForFunction(() => {
const {createTestNameSelector, findAllNodes} = window.REACT_DOM_DEVTOOLS;
const container = document.getElementById('devtools');
const input = findAllNodes(container, [
createTestNameSelector('SnapshotSelector-Input'),
]);
return input.length === 1;
});
const text = await getSnapshotSelectorText();
expect(text).toBe('1 / 3');
await clickButtonAndVerifySnapshotSelecetorText(
'SnapshotSelector-NextButton',
'2 / 3'
);
await clickButtonAndVerifySnapshotSelecetorText(
'SnapshotSelector-NextButton',
'3 / 3'
);
await clickButtonAndVerifySnapshotSelecetorText(
'SnapshotSelector-NextButton',
'1 / 3'
);
await clickButtonAndVerifySnapshotSelecetorText(
'SnapshotSelector-PreviousButton',
'3 / 3'
);
await clickButtonAndVerifySnapshotSelecetorText(
'SnapshotSelector-PreviousButton',
'2 / 3'
);
await clickButtonAndVerifySnapshotSelecetorText(
'SnapshotSelector-PreviousButton',
'1 / 3'
);
await clickButtonAndVerifySnapshotSelecetorText(
'SnapshotSelector-PreviousButton',
'3 / 3'
);
});
});

View File

@@ -1,8 +1,10 @@
const config = {
use: {
headless: false,
headless: true,
browserName: 'chromium',
launchOptions: {
// This bit of delay gives async React time to render
// and DevTools operations to be sent across the bridge.
slowMo: 100,
},
},

View File

@@ -15,6 +15,7 @@ import Tooltip from './Components/reach-ui/tooltip';
type Props = {
children: React$Node,
className?: string,
testName?: ?string,
title: React$Node,
...
};
@@ -22,11 +23,15 @@ type Props = {
export default function Button({
children,
className = '',
testName,
title,
...rest
}: Props) {
let button = (
<button className={`${styles.Button} ${className}`} {...rest}>
<button
className={`${styles.Button} ${className}`}
data-testname={testName}
{...rest}>
<span className={`${styles.ButtonContent} ${className}`} tabIndex={-1}>
{children}
</span>

View File

@@ -33,6 +33,7 @@ export default function ComponentSearchInput(props: Props) {
searchIndex={searchIndex}
searchResultsCount={searchResults.length}
searchText={searchText}
testName="ComponentSearchInput"
/>
);
}

View File

@@ -93,6 +93,7 @@ export default function EditableName({
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder="new entry"
testName="EditableName"
type="text"
value={editableName}
/>

View File

@@ -94,6 +94,7 @@ export default function EditableValue({
<input
autoComplete="new-password"
className={`${isValid ? styles.Input : styles.Invalid} ${className}`}
data-testname="EditableValue"
onBlur={applyChanges}
onChange={handleChange}
onKeyDown={handleKeyDown}

View File

@@ -74,7 +74,7 @@ export default function Element({data, index, style}: Props) {
}
};
const handleMouseDown = ({metaKey}) => {
const handleClick = ({metaKey}) => {
if (id !== null) {
dispatch({
type: 'SELECT_ELEMENT_BY_ID',
@@ -132,9 +132,10 @@ export default function Element({data, index, style}: Props) {
className={className}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onMouseDown={handleMouseDown}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
style={style}
data-testname="ComponentTreeListItem"
data-depth={depth}>
{/* This wrapper is used by Tree for measurement purposes. */}
<div

View File

@@ -252,7 +252,7 @@ export default function InspectedElementWrapper(_: Props) {
return (
<div className={styles.InspectedElement}>
<div className={styles.TitleRow}>
<div className={styles.TitleRow} data-testname="InspectedElement-Title">
{strictModeBadge}
{element.key && (

View File

@@ -85,7 +85,9 @@ export function InspectedElementHooksTree({
return null;
} else {
return (
<div className={styles.HooksTreeView}>
<div
className={styles.HooksTreeView}
data-testname="InspectedElementHooksTree">
<div className={styles.HeaderRow}>
<div className={styles.Header}>hooks</div>
{enableNamedHooksFeature &&
@@ -96,6 +98,7 @@ export function InspectedElementHooksTree({
isChecked={parseHookNamesOptimistic}
isDisabled={parseHookNamesOptimistic || hookParsingFailed}
onChange={handleChange}
testName="LoadHookNamesButton"
title={toggleTitle}>
<ButtonIcon type="parse-hook-names" />
</Toggle>

View File

@@ -63,7 +63,9 @@ export default function InspectedElementPropsTree({
const handleCopy = () => copy(serializeDataForCopy(((props: any): Object)));
return (
<div className={styles.InspectedElementTree}>
<div
className={styles.InspectedElementTree}
data-testname="InspectedElementPropsTree">
<div className={styles.HeaderRow}>
<div className={styles.Header}>props</div>
{!isEmpty && (

View File

@@ -145,7 +145,9 @@ export default function InspectedElementView({
<NativeStyleEditor />
{showRenderedBy && (
<div className={styles.Owners}>
<div
className={styles.Owners}
data-testname="InspectedElementView-Owners">
<div className={styles.OwnersHeader}>rendered by</div>
{showOwnersList &&
((owners: any): Array<SerializedElement>).map(owner => (
@@ -264,7 +266,7 @@ type SourceProps = {|
function Source({fileName, lineNumber}: SourceProps) {
const handleCopy = () => copy(`${fileName}:${lineNumber}`);
return (
<div className={styles.Source}>
<div className={styles.Source} data-testname="InspectedElementView-Source">
<div className={styles.SourceHeaderRow}>
<div className={styles.SourceHeader}>source</div>
<Button onClick={handleCopy} title="Copy to clipboard">

View File

@@ -14,6 +14,7 @@ type Props = {
className?: string,
onFocus?: (event: FocusEvent) => void,
placeholder?: string,
testName?: ?string,
value: any,
...
};
@@ -22,6 +23,7 @@ export default function AutoSizeInput({
className,
onFocus,
placeholder = '',
testName,
value,
...rest
}: Props) {
@@ -42,6 +44,7 @@ export default function AutoSizeInput({
return (
<input
className={[styles.Input, className].join(' ')}
data-testname={testName}
onFocus={onFocusWrapper}
placeholder={placeholder}
style={{

View File

@@ -36,6 +36,7 @@ export default function RecordToggle({disabled}: Props) {
className={className}
disabled={disabled}
onClick={isProfiling ? stopProfiling : startProfiling}
testName="ProfilerToggleButton"
title={isProfiling ? 'Stop profiling' : 'Start profiling'}>
<ButtonIcon type="record" />
</Button>

View File

@@ -122,6 +122,7 @@ export default function SnapshotSelector(_: Props) {
const input = (
<input
className={styles.Input}
data-testname="SnapshotSelector-Input"
type="text"
inputMode="numeric"
pattern="[0-9]*"
@@ -176,9 +177,14 @@ export default function SnapshotSelector(_: Props) {
return (
<Fragment>
<span className={styles.IndexLabel}>{label}</span>
<span
className={styles.IndexLabel}
data-testname="SnapshotSelector-Label">
{label}
</span>
<Button
className={styles.Button}
data-testname="SnapshotSelector-PreviousButton"
disabled={numFilteredCommits === 0}
onClick={viewPrevCommit}
title="Select previous commit">
@@ -212,6 +218,7 @@ export default function SnapshotSelector(_: Props) {
</div>
<Button
className={styles.Button}
data-testname="SnapshotSelector-NextButton"
disabled={numFilteredCommits === 0}
onClick={viewNextCommit}
title="Select next commit">

View File

@@ -23,6 +23,7 @@ type Props = {|
searchIndex: number,
searchResultsCount: number,
searchText: string,
testName?: ?string,
|};
export default function SearchInput({
@@ -33,6 +34,7 @@ export default function SearchInput({
searchIndex,
searchResultsCount,
searchText,
testName,
}: Props) {
const inputRef = useRef<HTMLInputElement | null>(null);
@@ -78,9 +80,10 @@ export default function SearchInput({
}, []);
return (
<div className={styles.SearchInput}>
<div className={styles.SearchInput} data-testname={testName}>
<Icon className={styles.InputIcon} type="search" />
<input
data-testname={testName ? `${testName}-Input` : undefined}
className={styles.Input}
onChange={handleChange}
onKeyPress={handleKeyPress}
@@ -90,12 +93,15 @@ export default function SearchInput({
/>
{!!searchText && (
<React.Fragment>
<span className={styles.IndexLabel}>
<span
className={styles.IndexLabel}
data-testname={testName ? `${testName}-ResultsCount` : undefined}>
{Math.min(searchIndex + 1, searchResultsCount)} |{' '}
{searchResultsCount}
</span>
<div className={styles.LeftVRule} />
<Button
data-testname={testName ? `${testName}-PreviousButton` : undefined}
className={styles.IconButton}
disabled={!searchText}
onClick={goToPreviousResult}
@@ -108,6 +114,7 @@ export default function SearchInput({
<ButtonIcon type="up" />
</Button>
<Button
data-testname={testName ? `${testName}-NextButton` : undefined}
className={styles.IconButton}
disabled={!searchText}
onClick={goToNextResult}
@@ -119,6 +126,7 @@ export default function SearchInput({
<ButtonIcon type="down" />
</Button>
<Button
data-testname={testName ? `${testName}-ResetButton` : undefined}
className={styles.IconButton}
disabled={!searchText}
onClick={resetSearch}

View File

@@ -102,6 +102,7 @@ export default function TabBar({
disabled ? styles.TabDisabled : styles.Tab,
!disabled && currentTab === id ? styles.TabCurrent : '',
].join(' ')}
data-testname={`TabBarButton-${id}`}
key={id}
onKeyDown={handleKeyDown}
onMouseDown={() => selectTab(id)}>

View File

@@ -19,6 +19,7 @@ type Props = {
isChecked: boolean,
isDisabled?: boolean,
onChange: (isChecked: boolean) => void,
testName?: ?string,
title?: string,
...
};
@@ -29,6 +30,7 @@ export default function Toggle({
isDisabled = false,
isChecked,
onChange,
testName,
title,
}: Props) {
let defaultClassName;
@@ -48,6 +50,7 @@ export default function Toggle({
let toggle = (
<button
className={`${defaultClassName} ${className}`}
data-testname={testName}
disabled={isDisabled}
onClick={handleClick}>
<span className={styles.ToggleContent} tabIndex={-1}>

View File

@@ -2,12 +2,8 @@
// This test harness mounts each test app as a separate root to test multi-root applications.
import {createElement} from 'react';
import {
// $FlowFixMe Flow does not yet know about createRoot()
createRoot,
} from 'react-dom';
import ToDoList from '../app/ToDoList';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
const container = document.createElement('div');
@@ -15,6 +11,11 @@ const container = document.createElement('div');
// TODO We may want to parameterize this app
// so that it can load things other than just ToDoList.
const App = require('./apps/ListApp').default;
const root = createRoot(container);
root.render(createElement(ToDoList));
// $FlowFixMe Flow doesn't know about createRoot() yet.
const root = ReactDOM.createRoot(container);
root.render(<App />);
// ReactDOM Test Selector APIs used by Playwright e2e tests
window.parent.REACT_DOM_APP = ReactDOM;

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
import {useRef, useState} from 'react';
export default function App() {
return <List />;
}
function List() {
const [items, setItems] = useState(['one', 'two', 'three']);
const inputRef = useRef(null);
const addItem = () => {
const input = ((inputRef.current: any): HTMLInputElement);
const text = input.value;
input.value = '';
if (text) {
setItems([...items, text]);
}
};
return (
<>
<input ref={inputRef} data-testname="AddItemInput" />
<button data-testname="AddItemButton" onClick={addItem}>
Add Item
</button>
<ul data-testname="List">
{items.map((label, index) => (
<ListItem key={index} label={label} />
))}
</ul>
</>
);
}
function ListItem({label}) {
return <li data-testname="ListItem">{label}</li>;
}

View File

@@ -1,11 +1,21 @@
import * as React from 'react';
import {createRoot} from 'react-dom';
import * as ReactDOM from 'react-dom';
import {
activate as activateBackend,
initialize as initializeBackend,
} from 'react-devtools-inline/backend';
import {initialize as createDevTools} from 'react-devtools-inline/frontend';
// This is a pretty gross hack to make the runtime loaded named-hooks-code work.
// TODO (Webpack 5) Hoepfully we can remove this once we upgrade to Webpack 5.
// $FlowFixMe
__webpack_public_path__ = '/dist/'; // eslint-disable-line no-undef
// TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration.
function hookNamesModuleLoaderFunction() {
return import('react-devtools-inline/hookNames');
}
function inject(contentDocument, sourcePath, callback) {
const script = contentDocument.createElement('script');
script.onload = callback;
@@ -22,7 +32,13 @@ function init(appIframe, devtoolsContainer, appSource) {
const DevTools = createDevTools(contentWindow);
inject(contentDocument, appSource, () => {
createRoot(devtoolsContainer).render(<DevTools />);
// $FlowFixMe Flow doesn't know about createRoot() yet.
ReactDOM.createRoot(devtoolsContainer).render(
<DevTools
hookNamesModuleLoaderFunction={hookNamesModuleLoaderFunction}
showTabBar={true}
/>,
);
});
activateBackend(contentWindow);
@@ -32,3 +48,6 @@ const iframe = document.getElementById('iframe');
const devtoolsContainer = document.getElementById('devtools');
init(iframe, devtoolsContainer, 'dist/e2e-app.js');
// ReactDOM Test Selector APIs used by Playwright e2e tests
window.parent.REACT_DOM_DEVTOOLS = ReactDOM;