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;