@samitouri / QOS-React-2 / commits / a4ead704ba

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

Brian Vaughn committed Dec 21, 2021 at 11:58 UTC a4ead704ba1df8718d4866a23ac25e78f8ea901a
24 files changed +549 -74
packages/react-devtools-inline/__tests__/__e2e__/components.test.js new
+206
@@ -0,0 +1,206 @@
1 +/** @flow */
2 +
3 +'use strict';
4 +
5 +const listAppUtils = require('./list-app-utils');
6 +const devToolsUtils = require('./devtools-utils');
7 +const {test, expect} = require('@playwright/test');
8 +const config = require('../../playwright.config');
9 +test.use(config);
10 +test.describe('Components', () => {
11 + let page;
12 +
13 + test.beforeEach(async ({browser}) => {
14 + page = await browser.newPage();
15 +
16 + await page.goto('http://localhost:8080/e2e.html', {
17 + waitUntil: 'domcontentloaded',
18 + });
19 +
20 + await page.waitForSelector('#iframe');
21 +
22 + await devToolsUtils.clickButton(page, 'TabBarButton-components');
23 + });
24 +
25 + test('Should display initial React components', async () => {
26 + const appRowCount = await page.evaluate(() => {
27 + const {createTestNameSelector, findAllNodes} = window.REACT_DOM_APP;
28 + const container = document.getElementById('iframe').contentDocument;
29 + const rows = findAllNodes(container, [
30 + createTestNameSelector('ListItem'),
31 + ]);
32 + return rows.length;
33 + });
34 + expect(appRowCount).toBe(3);
35 +
36 + const devToolsRowCount = await devToolsUtils.getElementCount(
37 + page,
38 + 'ListItem'
39 + );
40 + expect(devToolsRowCount).toBe(3);
41 + });
42 +
43 + test('Should display newly added React components', async () => {
44 + await listAppUtils.addItem(page, 'four');
45 +
46 + const count = await devToolsUtils.getElementCount(page, 'ListItem');
47 + expect(count).toBe(4);
48 + });
49 +
50 + test('Should allow elements to be inspected', async () => {
51 + // Select the first list item in DevTools.
52 + await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp');
53 +
54 + // Then read the inspected values.
55 + const [propName, propValue, sourceText] = await page.evaluate(() => {
56 + const {createTestNameSelector, findAllNodes} = window.REACT_DOM_DEVTOOLS;
57 + const container = document.getElementById('devtools');
58 +
59 + const editableName = findAllNodes(container, [
60 + createTestNameSelector('InspectedElementPropsTree'),
61 + createTestNameSelector('EditableName'),
62 + ])[0];
63 + const editableValue = findAllNodes(container, [
64 + createTestNameSelector('InspectedElementPropsTree'),
65 + createTestNameSelector('EditableValue'),
66 + ])[0];
67 + const source = findAllNodes(container, [
68 + createTestNameSelector('InspectedElementView-Source'),
69 + ])[0];
70 +
71 + return [editableName.value, editableValue.value, source.innerText];
72 + });
73 +
74 + expect(propName).toBe('label');
75 + expect(propValue).toBe('"one"');
76 + expect(sourceText).toContain('ListApp.js');
77 + });
78 +
79 + test('should allow props to be edited', async () => {
80 + // Select the first list item in DevTools.
81 + await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp');
82 +
83 + // Then edit the label prop.
84 + await page.evaluate(() => {
85 + const {createTestNameSelector, focusWithin} = window.REACT_DOM_DEVTOOLS;
86 + const container = document.getElementById('devtools');
87 +
88 + focusWithin(container, [
89 + createTestNameSelector('InspectedElementPropsTree'),
90 + createTestNameSelector('EditableValue'),
91 + ]);
92 + });
93 +
94 + page.keyboard.press('Backspace'); // "
95 + page.keyboard.press('Backspace'); // e
96 + page.keyboard.press('Backspace'); // n
97 + page.keyboard.press('Backspace'); // o
98 + page.keyboard.insertText('new"');
99 + page.keyboard.press('Enter');
100 +
101 + await page.waitForFunction(() => {
102 + const {createTestNameSelector, findAllNodes} = window.REACT_DOM_APP;
103 + const container = document.getElementById('iframe').contentDocument;
104 + const rows = findAllNodes(container, [
105 + createTestNameSelector('ListItem'),
106 + ])[0];
107 + return rows.innerText === 'new';
108 + });
109 + });
110 +
111 + test('should load and parse hook names for the inspected element', async () => {
112 + // Select the List component DevTools.
113 + await devToolsUtils.selectElement(page, 'List', 'App');
114 +
115 + // Then click to load and parse hook names.
116 + await devToolsUtils.clickButton(page, 'LoadHookNamesButton');
117 +
118 + // Make sure the expected hook names are parsed and displayed eventually.
119 + await page.waitForFunction(
120 + hookNames => {
121 + const {
122 + createTestNameSelector,
123 + findAllNodes,
124 + } = window.REACT_DOM_DEVTOOLS;
125 + const container = document.getElementById('devtools');
126 +
127 + const hooksTree = findAllNodes(container, [
128 + createTestNameSelector('InspectedElementHooksTree'),
129 + ])[0];
130 +
131 + if (!hooksTree) {
132 + return false;
133 + }
134 +
135 + const hooksTreeText = hooksTree.innerText;
136 +
137 + for (let i = 0; i < hookNames.length; i++) {
138 + if (!hooksTreeText.includes(hookNames[i])) {
139 + return false;
140 + }
141 + }
142 +
143 + return true;
144 + },
145 + ['State(items)', 'Ref(inputRef)']
146 + );
147 + });
148 +
149 + test('should allow searching for component by name', async () => {
150 + async function getComponentSearchResultsCount() {
151 + return await page.evaluate(() => {
152 + const {
153 + createTestNameSelector,
154 + findAllNodes,
155 + } = window.REACT_DOM_DEVTOOLS;
156 + const container = document.getElementById('devtools');
157 +
158 + const element = findAllNodes(container, [
159 + createTestNameSelector('ComponentSearchInput-ResultsCount'),
160 + ])[0];
161 + return element.innerText;
162 + });
163 + }
164 +
165 + await page.evaluate(() => {
166 + const {createTestNameSelector, focusWithin} = window.REACT_DOM_DEVTOOLS;
167 + const container = document.getElementById('devtools');
168 +
169 + focusWithin(container, [
170 + createTestNameSelector('ComponentSearchInput-Input'),
171 + ]);
172 + });
173 +
174 + page.keyboard.insertText('List');
175 + let count = await getComponentSearchResultsCount();
176 + expect(count).toBe('1 | 4');
177 +
178 + page.keyboard.insertText('Item');
179 + count = await getComponentSearchResultsCount();
180 + expect(count).toBe('1 | 3');
181 +
182 + page.keyboard.press('Enter');
183 + count = await getComponentSearchResultsCount();
184 + expect(count).toBe('2 | 3');
185 +
186 + page.keyboard.press('Enter');
187 + count = await getComponentSearchResultsCount();
188 + expect(count).toBe('3 | 3');
189 +
190 + page.keyboard.press('Enter');
191 + count = await getComponentSearchResultsCount();
192 + expect(count).toBe('1 | 3');
193 +
194 + page.keyboard.press('Shift+Enter');
195 + count = await getComponentSearchResultsCount();
196 + expect(count).toBe('3 | 3');
197 +
198 + page.keyboard.press('Shift+Enter');
199 + count = await getComponentSearchResultsCount();
200 + expect(count).toBe('2 | 3');
201 +
202 + page.keyboard.press('Shift+Enter');
203 + count = await getComponentSearchResultsCount();
204 + expect(count).toBe('1 | 3');
205 + });
206 +});
packages/react-devtools-inline/__tests__/__e2e__/devtools-utils.js new
+83
@@ -0,0 +1,83 @@
1 +'use strict';
2 +
3 +/** @flow */
4 +
5 +async function clickButton(page, buttonTestName) {
6 + await page.evaluate(testName => {
7 + const {createTestNameSelector, findAllNodes} = window.REACT_DOM_DEVTOOLS;
8 + const container = document.getElementById('devtools');
9 +
10 + const button = findAllNodes(container, [
11 + createTestNameSelector(testName),
12 + ])[0];
13 + button.click();
14 + }, buttonTestName);
15 +}
16 +
17 +async function getElementCount(page, displayName) {
18 + return await page.evaluate(listItemText => {
19 + const {
20 + createTestNameSelector,
21 + createTextSelector,
22 + findAllNodes,
23 + } = window.REACT_DOM_DEVTOOLS;
24 + const container = document.getElementById('devtools');
25 + const rows = findAllNodes(container, [
26 + createTestNameSelector('ComponentTreeListItem'),
27 + createTextSelector(listItemText),
28 + ]);
29 + return rows.length;
30 + }, displayName);
31 +}
32 +
33 +async function selectElement(page, displayName, waitForOwnersText) {
34 + await page.evaluate(listItemText => {
35 + const {
36 + createTestNameSelector,
37 + createTextSelector,
38 + findAllNodes,
39 + } = window.REACT_DOM_DEVTOOLS;
40 + const container = document.getElementById('devtools');
41 +
42 + const listItem = findAllNodes(container, [
43 + createTestNameSelector('ComponentTreeListItem'),
44 + createTextSelector(listItemText),
45 + ])[0];
46 + listItem.click();
47 + }, displayName);
48 +
49 + if (waitForOwnersText) {
50 + // Wait for selected element's props to load.
51 + await page.waitForFunction(
52 + ({titleText, ownersListText}) => {
53 + const {
54 + createTestNameSelector,
55 + findAllNodes,
56 + } = window.REACT_DOM_DEVTOOLS;
57 + const container = document.getElementById('devtools');
58 +
59 + const title = findAllNodes(container, [
60 + createTestNameSelector('InspectedElement-Title'),
61 + ])[0];
62 +
63 + const ownersList = findAllNodes(container, [
64 + createTestNameSelector('InspectedElementView-Owners'),
65 + ])[0];
66 +
67 + return (
68 + title &&
69 + title.innerText.includes(titleText) &&
70 + ownersList &&
71 + ownersList.innerText.includes(ownersListText)
72 + );
73 + },
74 + {titleText: displayName, ownersListText: waitForOwnersText}
75 + );
76 + }
77 +}
78 +
79 +module.exports = {
80 + clickButton,
81 + getElementCount,
82 + selectElement,
83 +};
packages/react-devtools-inline/__tests__/__e2e__/inspecting-props.test.js deleted
-52
@@ -1,52 +0,0 @@
1 -'use strict';
2 -
3 -const {test, expect} = require('@playwright/test');
4 -const config = require('../../playwright.config');
5 -test.use(config);
6 -
7 -test.describe('Testing Todo-List App', () => {
8 - let page, frameElementHandle, frame;
9 - test.beforeAll(async ({browser}) => {
10 - page = await browser.newPage();
11 - await page.goto('http://localhost:8080/e2e.html', {
12 - waitUntil: 'domcontentloaded',
13 - });
14 - await page.waitForSelector('iframe#iframe');
15 - frameElementHandle = await page.$('#iframe');
16 - frame = await frameElementHandle.contentFrame();
17 - });
18 -
19 - test('The Todo List should contain 3 items by default', async () => {
20 - const list = frame.locator('.listitem');
21 - await expect(list).toHaveCount(3);
22 - });
23 -
24 - test('Add another item Fourth to list', async () => {
25 - await frame.type('.input', 'Fourth');
26 - await frame.click('button.iconbutton');
27 - const listItems = await frame.locator('.label');
28 - await expect(listItems).toHaveText(['First', 'Second', 'Third', 'Fourth']);
29 - });
30 -
31 - test('Inspecting list elements with devtools', async () => {
32 - // Component props are used as string in devtools.
33 - const listItemsProps = [
34 - '',
35 - '{id: 1, isComplete: true, text: "First"}',
36 - '{id: 2, isComplete: true, text: "Second"}',
37 - '{id: 3, isComplete: false, text: "Third"}',
38 - '{id: 4, isComplete: false, text: "Fourth"}',
39 - ];
40 - const countOfItems = await frame.$$eval('.listitem', el => el.length);
41 - // For every item in list click on devtools inspect icon
42 - // click on the list item to quickly navigate to the list item component in devtools
43 - // comparing displayed props with the array of props.
44 - for (let i = 1; i <= countOfItems; ++i) {
45 - await page.click('[class^=ToggleContent]', {delay: 100});
46 - await frame.click(`.listitem:nth-child(${i})`, {delay: 50});
47 - await page.waitForSelector('span[class^=Value]');
48 - const text = await page.innerText('span[class^=Value]');
49 - await expect(text).toEqual(listItemsProps[i]);
50 - }
51 - });
52 -});
packages/react-devtools-inline/__tests__/__e2e__/list-app-utils.js new
+25
@@ -0,0 +1,25 @@
1 +'use strict';
2 +
3 +/** @flow */
4 +
5 +async function addItem(page, newItemText) {
6 + await page.evaluate(text => {
7 + const {createTestNameSelector, findAllNodes} = window.REACT_DOM_APP;
8 + const container = document.getElementById('iframe').contentDocument;
9 +
10 + const input = findAllNodes(container, [
11 + createTestNameSelector('AddItemInput'),
12 + ])[0];
13 + input.value = text;
14 +
15 + const button = findAllNodes(container, [
16 + createTestNameSelector('AddItemButton'),
17 + ])[0];
18 +
19 + button.click();
20 + }, newItemText);
21 +}
22 +
23 +module.exports = {
24 + addItem,
25 +};
packages/react-devtools-inline/__tests__/__e2e__/profiler.test.js new
+104
@@ -0,0 +1,104 @@
1 +/** @flow */
2 +
3 +'use strict';
4 +
5 +const listAppUtils = require('./list-app-utils');
6 +const devToolsUtils = require('./devtools-utils');
7 +const {test, expect} = require('@playwright/test');
8 +const config = require('../../playwright.config');
9 +test.use(config);
10 +test.describe('Profiler', () => {
11 + let page;
12 +
13 + test.beforeEach(async ({browser}) => {
14 + page = await browser.newPage();
15 +
16 + await page.goto('http://localhost:8080/e2e.html', {
17 + waitUntil: 'domcontentloaded',
18 + });
19 +
20 + await page.waitForSelector('#iframe');
21 +
22 + await devToolsUtils.clickButton(page, 'TabBarButton-profiler');
23 + });
24 +
25 + test('should record renders and commits when active', async () => {
26 + async function getSnapshotSelectorText() {
27 + return await page.evaluate(() => {
28 + const {
29 + createTestNameSelector,
30 + findAllNodes,
31 + } = window.REACT_DOM_DEVTOOLS;
32 + const container = document.getElementById('devtools');
33 +
34 + const input = findAllNodes(container, [
35 + createTestNameSelector('SnapshotSelector-Input'),
36 + ])[0];
37 + const label = findAllNodes(container, [
38 + createTestNameSelector('SnapshotSelector-Label'),
39 + ])[0];
40 + return `${input.value}${label.innerText}`;
41 + });
42 + }
43 +
44 + async function clickButtonAndVerifySnapshotSelecetorText(
45 + buttonTagName,
46 + expectedText
47 + ) {
48 + await devToolsUtils.clickButton(page, buttonTagName);
49 + const text = await getSnapshotSelectorText();
50 + expect(text).toBe(expectedText);
51 + }
52 +
53 + await devToolsUtils.clickButton(page, 'ProfilerToggleButton');
54 +
55 + await listAppUtils.addItem(page, 'four');
56 + await listAppUtils.addItem(page, 'five');
57 + await listAppUtils.addItem(page, 'six');
58 +
59 + await devToolsUtils.clickButton(page, 'ProfilerToggleButton');
60 +
61 + await page.waitForFunction(() => {
62 + const {createTestNameSelector, findAllNodes} = window.REACT_DOM_DEVTOOLS;
63 + const container = document.getElementById('devtools');
64 +
65 + const input = findAllNodes(container, [
66 + createTestNameSelector('SnapshotSelector-Input'),
67 + ]);
68 +
69 + return input.length === 1;
70 + });
71 +
72 + const text = await getSnapshotSelectorText();
73 + expect(text).toBe('1 / 3');
74 +
75 + await clickButtonAndVerifySnapshotSelecetorText(
76 + 'SnapshotSelector-NextButton',
77 + '2 / 3'
78 + );
79 + await clickButtonAndVerifySnapshotSelecetorText(
80 + 'SnapshotSelector-NextButton',
81 + '3 / 3'
82 + );
83 + await clickButtonAndVerifySnapshotSelecetorText(
84 + 'SnapshotSelector-NextButton',
85 + '1 / 3'
86 + );
87 + await clickButtonAndVerifySnapshotSelecetorText(
88 + 'SnapshotSelector-PreviousButton',
89 + '3 / 3'
90 + );
91 + await clickButtonAndVerifySnapshotSelecetorText(
92 + 'SnapshotSelector-PreviousButton',
93 + '2 / 3'
94 + );
95 + await clickButtonAndVerifySnapshotSelecetorText(
96 + 'SnapshotSelector-PreviousButton',
97 + '1 / 3'
98 + );
99 + await clickButtonAndVerifySnapshotSelecetorText(
100 + 'SnapshotSelector-PreviousButton',
101 + '3 / 3'
102 + );
103 + });
104 +});
packages/react-devtools-inline/playwright.config.js
+3 -1
@@ -1,8 +1,10 @@
1 const config = {
2 use: {
3 - headless: false,
3 + headless: true,
4 browserName: 'chromium',
5 launchOptions: {
6 + // This bit of delay gives async React time to render
7 + // and DevTools operations to be sent across the bridge.
8 slowMo: 100,
9 },
10 },
packages/react-devtools-shared/src/devtools/views/Button.js
+6 -1
@@ -15,6 +15,7 @@ import Tooltip from './Components/reach-ui/tooltip';
15 type Props = {
16 children: React$Node,
17 className?: string,
18 + testName?: ?string,
19 title: React$Node,
20 ...
21 };
@@ -22,11 +23,15 @@ type Props = {
23 export default function Button({
24 children,
25 className = '',
26 + testName,
27 title,
28 ...rest
29 }: Props) {
30 let button = (
29 - <button className={`${styles.Button} ${className}`} {...rest}>
31 + <button
32 + className={`${styles.Button} ${className}`}
33 + data-testname={testName}
34 + {...rest}>
35 <span className={`${styles.ButtonContent} ${className}`} tabIndex={-1}>
36 {children}
37 </span>
packages/react-devtools-shared/src/devtools/views/Components/ComponentSearchInput.js
+1
@@ -33,6 +33,7 @@ export default function ComponentSearchInput(props: Props) {
33 searchIndex={searchIndex}
34 searchResultsCount={searchResults.length}
35 searchText={searchText}
36 + testName="ComponentSearchInput"
37 />
38 );
39 }
packages/react-devtools-shared/src/devtools/views/Components/EditableName.js
+1
@@ -93,6 +93,7 @@ export default function EditableName({
93 onChange={handleChange}
94 onKeyDown={handleKeyDown}
95 placeholder="new entry"
96 + testName="EditableName"
97 type="text"
98 value={editableName}
99 />
packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js
+1
@@ -94,6 +94,7 @@ export default function EditableValue({
94 <input
95 autoComplete="new-password"
96 className={`${isValid ? styles.Input : styles.Invalid} ${className}`}
97 + data-testname="EditableValue"
98 onBlur={applyChanges}
99 onChange={handleChange}
100 onKeyDown={handleKeyDown}
packages/react-devtools-shared/src/devtools/views/Components/Element.js
+3 -2
@@ -74,7 +74,7 @@ export default function Element({data, index, style}: Props) {
74 }
75 };
76
77 - const handleMouseDown = ({metaKey}) => {
77 + const handleClick = ({metaKey}) => {
78 if (id !== null) {
79 dispatch({
80 type: 'SELECT_ELEMENT_BY_ID',
@@ -132,9 +132,10 @@ export default function Element({data, index, style}: Props) {
132 className={className}
133 onMouseEnter={handleMouseEnter}
134 onMouseLeave={handleMouseLeave}
135 - onMouseDown={handleMouseDown}
135 + onClick={handleClick}
136 onDoubleClick={handleDoubleClick}
137 style={style}
138 + data-testname="ComponentTreeListItem"
139 data-depth={depth}>
140 {/* This wrapper is used by Tree for measurement purposes. */}
141 <div
packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js
+1 -1
@@ -252,7 +252,7 @@ export default function InspectedElementWrapper(_: Props) {
252
253 return (
254 <div className={styles.InspectedElement}>
255 - <div className={styles.TitleRow}>
255 + <div className={styles.TitleRow} data-testname="InspectedElement-Title">
256 {strictModeBadge}
257
258 {element.key && (
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js
+4 -1
@@ -85,7 +85,9 @@ export function InspectedElementHooksTree({
85 return null;
86 } else {
87 return (
88 - <div className={styles.HooksTreeView}>
88 + <div
89 + className={styles.HooksTreeView}
90 + data-testname="InspectedElementHooksTree">
91 <div className={styles.HeaderRow}>
92 <div className={styles.Header}>hooks</div>
93 {enableNamedHooksFeature &&
@@ -96,6 +98,7 @@ export function InspectedElementHooksTree({
98 isChecked={parseHookNamesOptimistic}
99 isDisabled={parseHookNamesOptimistic || hookParsingFailed}
100 onChange={handleChange}
101 + testName="LoadHookNamesButton"
102 title={toggleTitle}>
103 <ButtonIcon type="parse-hook-names" />
104 </Toggle>
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementPropsTree.js
+3 -1
@@ -63,7 +63,9 @@ export default function InspectedElementPropsTree({
63 const handleCopy = () => copy(serializeDataForCopy(((props: any): Object)));
64
65 return (
66 - <div className={styles.InspectedElementTree}>
66 + <div
67 + className={styles.InspectedElementTree}
68 + data-testname="InspectedElementPropsTree">
69 <div className={styles.HeaderRow}>
70 <div className={styles.Header}>props</div>
71 {!isEmpty && (
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js
+4 -2
@@ -145,7 +145,9 @@ export default function InspectedElementView({
145 <NativeStyleEditor />
146
147 {showRenderedBy && (
148 - <div className={styles.Owners}>
148 + <div
149 + className={styles.Owners}
150 + data-testname="InspectedElementView-Owners">
151 <div className={styles.OwnersHeader}>rendered by</div>
152 {showOwnersList &&
153 ((owners: any): Array<SerializedElement>).map(owner => (
@@ -264,7 +266,7 @@ type SourceProps = {|
266 function Source({fileName, lineNumber}: SourceProps) {
267 const handleCopy = () => copy(`${fileName}:${lineNumber}`);
268 return (
267 - <div className={styles.Source}>
269 + <div className={styles.Source} data-testname="InspectedElementView-Source">
270 <div className={styles.SourceHeaderRow}>
271 <div className={styles.SourceHeader}>source</div>
272 <Button onClick={handleCopy} title="Copy to clipboard">
packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/AutoSizeInput.js
+3
@@ -14,6 +14,7 @@ type Props = {
14 className?: string,
15 onFocus?: (event: FocusEvent) => void,
16 placeholder?: string,
17 + testName?: ?string,
18 value: any,
19 ...
20 };
@@ -22,6 +23,7 @@ export default function AutoSizeInput({
23 className,
24 onFocus,
25 placeholder = '',
26 + testName,
27 value,
28 ...rest
29 }: Props) {
@@ -42,6 +44,7 @@ export default function AutoSizeInput({
44 return (
45 <input
46 className={[styles.Input, className].join(' ')}
47 + data-testname={testName}
48 onFocus={onFocusWrapper}
49 placeholder={placeholder}
50 style={{
packages/react-devtools-shared/src/devtools/views/Profiler/RecordToggle.js
+1
@@ -36,6 +36,7 @@ export default function RecordToggle({disabled}: Props) {
36 className={className}
37 disabled={disabled}
38 onClick={isProfiling ? stopProfiling : startProfiling}
39 + testName="ProfilerToggleButton"
40 title={isProfiling ? 'Stop profiling' : 'Start profiling'}>
41 <ButtonIcon type="record" />
42 </Button>
packages/react-devtools-shared/src/devtools/views/Profiler/SnapshotSelector.js
+8 -1
@@ -122,6 +122,7 @@ export default function SnapshotSelector(_: Props) {
122 const input = (
123 <input
124 className={styles.Input}
125 + data-testname="SnapshotSelector-Input"
126 type="text"
127 inputMode="numeric"
128 pattern="[0-9]*"
@@ -176,9 +177,14 @@ export default function SnapshotSelector(_: Props) {
177
178 return (
179 <Fragment>
179 - <span className={styles.IndexLabel}>{label}</span>
180 + <span
181 + className={styles.IndexLabel}
182 + data-testname="SnapshotSelector-Label">
183 + {label}
184 + </span>
185 <Button
186 className={styles.Button}
187 + data-testname="SnapshotSelector-PreviousButton"
188 disabled={numFilteredCommits === 0}
189 onClick={viewPrevCommit}
190 title="Select previous commit">
@@ -212,6 +218,7 @@ export default function SnapshotSelector(_: Props) {
218 </div>
219 <Button
220 className={styles.Button}
221 + data-testname="SnapshotSelector-NextButton"
222 disabled={numFilteredCommits === 0}
223 onClick={viewNextCommit}
224 title="Select next commit">
packages/react-devtools-shared/src/devtools/views/SearchInput.js
+10 -2
@@ -23,6 +23,7 @@ type Props = {|
23 searchIndex: number,
24 searchResultsCount: number,
25 searchText: string,
26 + testName?: ?string,
27 |};
28
29 export default function SearchInput({
@@ -33,6 +34,7 @@ export default function SearchInput({
34 searchIndex,
35 searchResultsCount,
36 searchText,
37 + testName,
38 }: Props) {
39 const inputRef = useRef<HTMLInputElement | null>(null);
40
@@ -78,9 +80,10 @@ export default function SearchInput({
80 }, []);
81
82 return (
81 - <div className={styles.SearchInput}>
83 + <div className={styles.SearchInput} data-testname={testName}>
84 <Icon className={styles.InputIcon} type="search" />
85 <input
86 + data-testname={testName ? `${testName}-Input` : undefined}
87 className={styles.Input}
88 onChange={handleChange}
89 onKeyPress={handleKeyPress}
@@ -90,12 +93,15 @@ export default function SearchInput({
93 />
94 {!!searchText && (
95 <React.Fragment>
93 - <span className={styles.IndexLabel}>
96 + <span
97 + className={styles.IndexLabel}
98 + data-testname={testName ? `${testName}-ResultsCount` : undefined}>
99 {Math.min(searchIndex + 1, searchResultsCount)} |{' '}
100 {searchResultsCount}
101 </span>
102 <div className={styles.LeftVRule} />
103 <Button
104 + data-testname={testName ? `${testName}-PreviousButton` : undefined}
105 className={styles.IconButton}
106 disabled={!searchText}
107 onClick={goToPreviousResult}
@@ -108,6 +114,7 @@ export default function SearchInput({
114 <ButtonIcon type="up" />
115 </Button>
116 <Button
117 + data-testname={testName ? `${testName}-NextButton` : undefined}
118 className={styles.IconButton}
119 disabled={!searchText}
120 onClick={goToNextResult}
@@ -119,6 +126,7 @@ export default function SearchInput({
126 <ButtonIcon type="down" />
127 </Button>
128 <Button
129 + data-testname={testName ? `${testName}-ResetButton` : undefined}
130 className={styles.IconButton}
131 disabled={!searchText}
132 onClick={resetSearch}
packages/react-devtools-shared/src/devtools/views/TabBar.js
+1
@@ -102,6 +102,7 @@ export default function TabBar({
102 disabled ? styles.TabDisabled : styles.Tab,
103 !disabled && currentTab === id ? styles.TabCurrent : '',
104 ].join(' ')}
105 + data-testname={`TabBarButton-${id}`}
106 key={id}
107 onKeyDown={handleKeyDown}
108 onMouseDown={() => selectTab(id)}>
packages/react-devtools-shared/src/devtools/views/Toggle.js
+3
@@ -19,6 +19,7 @@ type Props = {
19 isChecked: boolean,
20 isDisabled?: boolean,
21 onChange: (isChecked: boolean) => void,
22 + testName?: ?string,
23 title?: string,
24 ...
25 };
@@ -29,6 +30,7 @@ export default function Toggle({
30 isDisabled = false,
31 isChecked,
32 onChange,
33 + testName,
34 title,
35 }: Props) {
36 let defaultClassName;
@@ -48,6 +50,7 @@ export default function Toggle({
50 let toggle = (
51 <button
52 className={`${defaultClassName} ${className}`}
53 + data-testname={testName}
54 disabled={isDisabled}
55 onClick={handleClick}>
56 <span className={styles.ToggleContent} tabIndex={-1}>
packages/react-devtools-shell/src/e2e/app.js
+9 -8
@@ -2,12 +2,8 @@
2
3 // This test harness mounts each test app as a separate root to test multi-root applications.
4
5 -import {createElement} from 'react';
6 -import {
7 - // $FlowFixMe Flow does not yet know about createRoot()
8 - createRoot,
9 -} from 'react-dom';
10 -import ToDoList from '../app/ToDoList';
5 +import * as React from 'react';
6 +import * as ReactDOM from 'react-dom';
7
8 const container = document.createElement('div');
9
@@ -15,6 +11,11 @@ const container = document.createElement('div');
11
12 // TODO We may want to parameterize this app
13 // so that it can load things other than just ToDoList.
14 +const App = require('./apps/ListApp').default;
15
19 -const root = createRoot(container);
20 -root.render(createElement(ToDoList));
16 +// $FlowFixMe Flow doesn't know about createRoot() yet.
17 +const root = ReactDOM.createRoot(container);
18 +root.render(<App />);
19 +
20 +// ReactDOM Test Selector APIs used by Playwright e2e tests
21 +window.parent.REACT_DOM_APP = ReactDOM;
packages/react-devtools-shell/src/e2e/apps/ListApp.js new
+48
@@ -0,0 +1,48 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import * as React from 'react';
11 +import {useRef, useState} from 'react';
12 +
13 +export default function App() {
14 + return <List />;
15 +}
16 +
17 +function List() {
18 + const [items, setItems] = useState(['one', 'two', 'three']);
19 + const inputRef = useRef(null);
20 +
21 + const addItem = () => {
22 + const input = ((inputRef.current: any): HTMLInputElement);
23 + const text = input.value;
24 + input.value = '';
25 +
26 + if (text) {
27 + setItems([...items, text]);
28 + }
29 + };
30 +
31 + return (
32 + <>
33 + <input ref={inputRef} data-testname="AddItemInput" />
34 + <button data-testname="AddItemButton" onClick={addItem}>
35 + Add Item
36 + </button>
37 + <ul data-testname="List">
38 + {items.map((label, index) => (
39 + <ListItem key={index} label={label} />
40 + ))}
41 + </ul>
42 + </>
43 + );
44 +}
45 +
46 +function ListItem({label}) {
47 + return <li data-testname="ListItem">{label}</li>;
48 +}
packages/react-devtools-shell/src/e2e/devtools.js
+21 -2
@@ -1,11 +1,21 @@
1 import * as React from 'react';
2 -import {createRoot} from 'react-dom';
2 +import * as ReactDOM from 'react-dom';
3 import {
4 activate as activateBackend,
5 initialize as initializeBackend,
6 } from 'react-devtools-inline/backend';
7 import {initialize as createDevTools} from 'react-devtools-inline/frontend';
8
9 +// This is a pretty gross hack to make the runtime loaded named-hooks-code work.
10 +// TODO (Webpack 5) Hoepfully we can remove this once we upgrade to Webpack 5.
11 +// $FlowFixMe
12 +__webpack_public_path__ = '/dist/'; // eslint-disable-line no-undef
13 +
14 +// TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration.
15 +function hookNamesModuleLoaderFunction() {
16 + return import('react-devtools-inline/hookNames');
17 +}
18 +
19 function inject(contentDocument, sourcePath, callback) {
20 const script = contentDocument.createElement('script');
21 script.onload = callback;
@@ -22,7 +32,13 @@ function init(appIframe, devtoolsContainer, appSource) {
32 const DevTools = createDevTools(contentWindow);
33
34 inject(contentDocument, appSource, () => {
25 - createRoot(devtoolsContainer).render(<DevTools />);
35 + // $FlowFixMe Flow doesn't know about createRoot() yet.
36 + ReactDOM.createRoot(devtoolsContainer).render(
37 + <DevTools
38 + hookNamesModuleLoaderFunction={hookNamesModuleLoaderFunction}
39 + showTabBar={true}
40 + />,
41 + );
42 });
43
44 activateBackend(contentWindow);
@@ -32,3 +48,6 @@ const iframe = document.getElementById('iframe');
48 const devtoolsContainer = document.getElementById('devtools');
49
50 init(iframe, devtoolsContainer, 'dist/e2e-app.js');
51 +
52 +// ReactDOM Test Selector APIs used by Playwright e2e tests
53 +window.parent.REACT_DOM_DEVTOOLS = ReactDOM;