mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
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:
@@ -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');
|
||||
});
|
||||
});
|
||||
83
packages/react-devtools-inline/__tests__/__e2e__/devtools-utils.js
vendored
Normal file
83
packages/react-devtools-inline/__tests__/__e2e__/devtools-utils.js
vendored
Normal 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,
|
||||
};
|
||||
@@ -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]);
|
||||
}
|
||||
});
|
||||
});
|
||||
25
packages/react-devtools-inline/__tests__/__e2e__/list-app-utils.js
vendored
Normal file
25
packages/react-devtools-inline/__tests__/__e2e__/list-app-utils.js
vendored
Normal 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,
|
||||
};
|
||||
@@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -33,6 +33,7 @@ export default function ComponentSearchInput(props: Props) {
|
||||
searchIndex={searchIndex}
|
||||
searchResultsCount={searchResults.length}
|
||||
searchText={searchText}
|
||||
testName="ComponentSearchInput"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ export default function EditableName({
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="new entry"
|
||||
testName="EditableName"
|
||||
type="text"
|
||||
value={editableName}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)}>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
17
packages/react-devtools-shell/src/e2e/app.js
vendored
17
packages/react-devtools-shell/src/e2e/app.js
vendored
@@ -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;
|
||||
|
||||
48
packages/react-devtools-shell/src/e2e/apps/ListApp.js
vendored
Normal file
48
packages/react-devtools-shell/src/e2e/apps/ListApp.js
vendored
Normal 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>;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user