@samitouri / QOS-React-2 / commits / 3133dfa6ee

DevTools] e2e Regression Testing App (#24619)

This PR adds an e2e regression app to the react-devtools-shell package. This app: * Has an app.js and an appLegacy.js entrypoint because apps prior to React 18 need to use ReactDOM.render. These files will create and render multiple test apps (though they currently only render the List) * Moved the ListApp out of the e2e folder and into an e2e-apps folder so that both e2e and e2e-regression can use the same test apps * Creates a ListAppLegacy app because prior to React 16.8 hooks didn't exist. * Added a devtools file for the e2e-regression * Modifies the webpack config so that the e2e-regression React app can use different a different React version than DevTools

Luna Ruan committed May 26, 2022 at 08:36 UTC 3133dfa6ee8d1a8355c0249007176955d4b7d6b0
10 files changed +358 -90
packages/react-devtools-shell/e2e-regression.html new
+40
@@ -0,0 +1,40 @@
1 +<!doctype html>
2 +<html>
3 + <head>
4 + <meta charset="utf8">
5 + <title>React DevTools</title>
6 + <meta name="viewport" content="width=device-width, initial-scale=1">
7 + <style>
8 + * {
9 + box-sizing: border-box;
10 + }
11 + body {
12 + margin: 0;
13 + padding: 0;
14 + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
15 + sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
16 + font-size: 12px;
17 + line-height: 1.5;
18 + }
19 + #iframe {
20 + position: absolute;
21 + top: 0;
22 + left: 0;
23 + width: 100vw;
24 + height: 50vh;
25 + }
26 + #devtools {
27 + position: absolute;
28 + bottom: 0;
29 + left: 0;
30 + width: 100vw;
31 + height: 50vh;
32 + }
33 + </style>
34 + </head>
35 + <body>
36 + <iframe id="iframe"></iframe>
37 + <div id="devtools"></div>
38 + <script src="dist/e2e-devtools-regression.js"></script>
39 + </body>
40 +</html>
\ No newline at end of file
packages/react-devtools-shell/index.html
+1
@@ -51,6 +51,7 @@
51 <a href="/multi.html">multi DevTools</a>
52 |
53 <a href="/e2e.html">e2e tests</a>
54 + <a href="/e2e-regression.html">e2e regression tests</a>
55 </span>
56 </div>
57
packages/react-devtools-shell/package.json
+2 -1
@@ -7,7 +7,8 @@
7 },
8 "dependencies": {
9 "immutable": "^4.0.0-rc.12",
10 - "react-native-web": "0.0.0-26873b469"
10 + "react-native-web": "0.0.0-26873b469",
11 + "semver": "^6.3.0"
12 },
13 "devDependencies": {
14 "@babel/core": "^7.11.1",
packages/react-devtools-shell/src/e2e-apps/ListApp.js renamed
packages/react-devtools-shell/src/e2e-apps/ListAppLegacy.js new
+55
@@ -0,0 +1,55 @@
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 +
12 +export default function App() {
13 + return <List />;
14 +}
15 +
16 +class List extends React.Component {
17 + constructor(props) {
18 + super(props);
19 + this.state = {
20 + items: ['one', 'two', 'three'],
21 + };
22 + }
23 +
24 + addItem = () => {
25 + if (this.inputRef && this.inputRef.value) {
26 + this.setState({items: [...this.state.items, this.inputRef.value]});
27 + this.inputRef.value = '';
28 + }
29 + };
30 +
31 + render() {
32 + return (
33 + <div>
34 + <input
35 + data-testname="AddItemInput"
36 + value={this.state.text}
37 + onChange={this.onInputChange}
38 + ref={c => (this.inputRef = c)}
39 + />
40 + <button data-testname="AddItemButton" onClick={this.addItem}>
41 + Add Item
42 + </button>
43 + <ul data-testname="List">
44 + {this.state.items.map((label, index) => (
45 + <ListItem key={index} label={label} />
46 + ))}
47 + </ul>
48 + </div>
49 + );
50 + }
51 +}
52 +
53 +function ListItem({label}) {
54 + return <li data-testname="ListItem">{label}</li>;
55 +}
packages/react-devtools-shell/src/e2e-regression/app-legacy.js new
+33
@@ -0,0 +1,33 @@
1 +/** @flow */
2 +
3 +// This test harness mounts each test app as a separate root to test multi-root applications.
4 +
5 +import * as React from 'react';
6 +import * as ReactDOM from 'react-dom';
7 +import {gte} from 'semver';
8 +import ListApp from '../e2e-apps/ListApp';
9 +import ListAppLegacy from '../e2e-apps/ListAppLegacy';
10 +const version = process.env.E2E_APP_REACT_VERSION;
11 +
12 +function mountApp(App) {
13 + const container = document.createElement('div');
14 +
15 + ((document.body: any): HTMLBodyElement).appendChild(container);
16 +
17 + ReactDOM.render(<App />, container);
18 +}
19 +function mountTestApp() {
20 + // ListApp has hooks, which aren't available until 16.8.0
21 + mountApp(gte(version, '16.8.0') ? ListApp : ListAppLegacy);
22 +}
23 +
24 +mountTestApp();
25 +
26 +// ReactDOM Test Selector APIs used by Playwright e2e tests
27 +// If they don't exist, we mock them
28 +window.parent.REACT_DOM_APP = {
29 + createTestNameSelector: name => `[data-testname="${name}"]`,
30 + findAllNodes: (container, nodes) =>
31 + container.querySelectorAll(nodes.join(' ')),
32 + ...ReactDOM,
33 +};
packages/react-devtools-shell/src/e2e-regression/app.js new
+25
@@ -0,0 +1,25 @@
1 +/** @flow */
2 +
3 +// This test harness mounts each test app as a separate root to test multi-root applications.
4 +
5 +import * as React from 'react';
6 +import * as ReactDOM from 'react-dom';
7 +import {createRoot} from 'react-dom/client';
8 +import ListApp from '../e2e-apps/ListApp';
9 +
10 +function mountApp(App) {
11 + const container = document.createElement('div');
12 +
13 + ((document.body: any): HTMLBodyElement).appendChild(container);
14 +
15 + const root = createRoot(container);
16 + root.render(<App />);
17 +}
18 +function mountTestApp() {
19 + mountApp(ListApp);
20 +}
21 +
22 +mountTestApp();
23 +
24 +// ReactDOM Test Selector APIs used by Playwright e2e tests
25 +window.parent.REACT_DOM_APP = ReactDOM;
packages/react-devtools-shell/src/e2e-regression/devtools.js new
+54
@@ -0,0 +1,54 @@
1 +import * as React from 'react';
2 +import * as ReactDOM from 'react-dom';
3 +import {createRoot} from 'react-dom/client';
4 +import {
5 + activate as activateBackend,
6 + initialize as initializeBackend,
7 +} from 'react-devtools-inline/backend';
8 +import {initialize as createDevTools} from 'react-devtools-inline/frontend';
9 +
10 +// This is a pretty gross hack to make the runtime loaded named-hooks-code work.
11 +// TODO (Webpack 5) Hoepfully we can remove this once we upgrade to Webpack 5.
12 +// $FlowFixMer
13 +__webpack_public_path__ = '/dist/'; // eslint-disable-line no-undef
14 +
15 +// TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration.
16 +function hookNamesModuleLoaderFunction() {
17 + return import('react-devtools-inline/hookNames');
18 +}
19 +
20 +function inject(contentDocument, sourcePath, callback) {
21 + const script = contentDocument.createElement('script');
22 + script.onload = callback;
23 + script.src = sourcePath;
24 +
25 + ((contentDocument.body: any): HTMLBodyElement).appendChild(script);
26 +}
27 +
28 +function init(appIframe, devtoolsContainer, appSource) {
29 + const {contentDocument, contentWindow} = appIframe;
30 +
31 + initializeBackend(contentWindow);
32 +
33 + const DevTools = createDevTools(contentWindow);
34 +
35 + inject(contentDocument, appSource, () => {
36 + // $FlowFixMe Flow doesn't know about createRoot() yet.
37 + createRoot(devtoolsContainer).render(
38 + <DevTools
39 + hookNamesModuleLoaderFunction={hookNamesModuleLoaderFunction}
40 + showTabBar={true}
41 + />,
42 + );
43 + });
44 +
45 + activateBackend(contentWindow);
46 +}
47 +
48 +const iframe = document.getElementById('iframe');
49 +const devtoolsContainer = document.getElementById('devtools');
50 +
51 +init(iframe, devtoolsContainer, 'dist/e2e-app-regression.js');
52 +
53 +// ReactDOM Test Selector APIs used by Playwright e2e tests
54 +window.parent.REACT_DOM_DEVTOOLS = ReactDOM;
packages/react-devtools-shell/src/e2e/app.js
+1 -1
@@ -12,7 +12,7 @@ const container = document.createElement('div');
12
13 // TODO We may want to parameterize this app
14 // so that it can load things other than just ToDoList.
15 -const App = require('./apps/ListApp').default;
15 +const App = require('../e2e-apps/ListApp').default;
16
17 const root = createRoot(container);
18 root.render(<App />);
packages/react-devtools-shell/webpack.config.js
+147 -88
@@ -1,5 +1,6 @@
1 const {resolve} = require('path');
2 const {DefinePlugin} = require('webpack');
3 +const fs = require('fs');
4 const {
5 DARK_MODE_DIMMED_WARNING_COLOR,
6 DARK_MODE_DIMMED_ERROR_COLOR,
@@ -11,6 +12,12 @@ const {
12 getVersionString,
13 } = require('react-devtools-extensions/utils');
14 const {resolveFeatureFlags} = require('react-devtools-shared/buildUtils');
15 +const semver = require('semver');
16 +
17 +const ReactVersionSrc = fs.readFileSync(require.resolve('shared/ReactVersion'));
18 +const currentReactVersion = /export default '([^']+)';/.exec(
19 + ReactVersionSrc,
20 +)[1];
21
22 const NODE_ENV = process.env.NODE_ENV;
23 if (!NODE_ENV) {
@@ -38,105 +45,157 @@ const __DEV__ = NODE_ENV === 'development';
45
46 const DEVTOOLS_VERSION = getVersionString();
47
41 -const config = {
42 - mode: __DEV__ ? 'development' : 'production',
43 - devtool: __DEV__ ? 'cheap-source-map' : 'source-map',
44 - entry: {
48 +// If the React version isn't set, we will use the
49 +// current React version instead. Likewise if the
50 +// React version isnt' set, we'll use the build folder
51 +// for both React DevTools and React
52 +const REACT_VERSION = process.env.REACT_VERSION
53 + ? semver.coerce(process.env.REACT_VERSION).version
54 + : currentReactVersion;
55 +
56 +const E2E_APP_BUILD_DIR = process.env.REACT_VERSION
57 + ? resolve(__dirname, '..', '..', 'build-regression', 'node_modules')
58 + : builtModulesDir;
59 +
60 +const makeConfig = (entry, alias) => {
61 + const config = {
62 + mode: __DEV__ ? 'development' : 'production',
63 + devtool: __DEV__ ? 'cheap-source-map' : 'source-map',
64 + entry,
65 + node: {
66 + // source-maps package has a dependency on 'fs'
67 + // but this build won't trigger that code path
68 + fs: 'empty',
69 + },
70 + resolve: {
71 + alias,
72 + },
73 + optimization: {
74 + minimize: false,
75 + },
76 + plugins: [
77 + new DefinePlugin({
78 + __DEV__,
79 + __EXPERIMENTAL__: true,
80 + __EXTENSION__: false,
81 + __PROFILE__: false,
82 + __TEST__: NODE_ENV === 'test',
83 + 'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
84 + 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,
85 + 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-shell"`,
86 + 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
87 + 'process.env.DARK_MODE_DIMMED_WARNING_COLOR': `"${DARK_MODE_DIMMED_WARNING_COLOR}"`,
88 + 'process.env.DARK_MODE_DIMMED_ERROR_COLOR': `"${DARK_MODE_DIMMED_ERROR_COLOR}"`,
89 + 'process.env.DARK_MODE_DIMMED_LOG_COLOR': `"${DARK_MODE_DIMMED_LOG_COLOR}"`,
90 + 'process.env.LIGHT_MODE_DIMMED_WARNING_COLOR': `"${LIGHT_MODE_DIMMED_WARNING_COLOR}"`,
91 + 'process.env.LIGHT_MODE_DIMMED_ERROR_COLOR': `"${LIGHT_MODE_DIMMED_ERROR_COLOR}"`,
92 + 'process.env.LIGHT_MODE_DIMMED_LOG_COLOR': `"${LIGHT_MODE_DIMMED_LOG_COLOR}"`,
93 + 'process.env.E2E_APP_REACT_VERSION': `"${REACT_VERSION}"`,
94 + }),
95 + ],
96 + module: {
97 + rules: [
98 + {
99 + test: /\.js$/,
100 + loader: 'babel-loader',
101 + options: {
102 + configFile: resolve(
103 + __dirname,
104 + '..',
105 + 'react-devtools-shared',
106 + 'babel.config.js',
107 + ),
108 + },
109 + },
110 + {
111 + test: /\.css$/,
112 + use: [
113 + {
114 + loader: 'style-loader',
115 + },
116 + {
117 + loader: 'css-loader',
118 + options: {
119 + sourceMap: true,
120 + modules: true,
121 + localIdentName: '[local]',
122 + },
123 + },
124 + ],
125 + },
126 + ],
127 + },
128 + };
129 +
130 + if (TARGET === 'local') {
131 + // Local dev server build.
132 + config.devServer = {
133 + hot: true,
134 + port: 8080,
135 + clientLogLevel: 'warning',
136 + publicPath: '/dist/',
137 + stats: 'errors-only',
138 + };
139 + } else {
140 + // Static build to deploy somewhere else.
141 + config.output = {
142 + path: resolve(__dirname, 'dist'),
143 + filename: '[name].js',
144 + };
145 + }
146 + return config;
147 +};
148 +
149 +const app = makeConfig(
150 + {
151 'app-index': './src/app/index.js',
152 'app-devtools': './src/app/devtools.js',
153 'e2e-app': './src/e2e/app.js',
154 'e2e-devtools': './src/e2e/devtools.js',
155 + 'e2e-devtools-regression': './src/e2e-regression/devtools.js',
156 'multi-left': './src/multi/left.js',
157 'multi-devtools': './src/multi/devtools.js',
158 'multi-right': './src/multi/right.js',
159 + 'e2e-regression': './src/e2e-regression/app.js',
160 },
53 - node: {
54 - // source-maps package has a dependency on 'fs'
55 - // but this build won't trigger that code path
56 - fs: 'empty',
57 - },
58 - resolve: {
59 - alias: {
60 - react: resolve(builtModulesDir, 'react'),
61 - 'react-debug-tools': resolve(builtModulesDir, 'react-debug-tools'),
62 - 'react-devtools-feature-flags': resolveFeatureFlags('shell'),
63 - 'react-dom/client': resolve(builtModulesDir, 'react-dom/client'),
64 - 'react-dom': resolve(builtModulesDir, 'react-dom/unstable_testing'),
65 - 'react-is': resolve(builtModulesDir, 'react-is'),
66 - scheduler: resolve(builtModulesDir, 'scheduler'),
67 - },
161 + {
162 + react: resolve(builtModulesDir, 'react'),
163 + 'react-debug-tools': resolve(builtModulesDir, 'react-debug-tools'),
164 + 'react-devtools-feature-flags': resolveFeatureFlags('shell'),
165 + 'react-dom/client': resolve(builtModulesDir, 'react-dom/client'),
166 + 'react-dom': resolve(builtModulesDir, 'react-dom/unstable_testing'),
167 + 'react-is': resolve(builtModulesDir, 'react-is'),
168 + scheduler: resolve(builtModulesDir, 'scheduler'),
169 },
69 - optimization: {
70 - minimize: false,
71 - },
72 - plugins: [
73 - new DefinePlugin({
74 - __DEV__,
75 - __EXPERIMENTAL__: true,
76 - __EXTENSION__: false,
77 - __PROFILE__: false,
78 - __TEST__: NODE_ENV === 'test',
79 - 'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
80 - 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,
81 - 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-shell"`,
82 - 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
83 - 'process.env.DARK_MODE_DIMMED_WARNING_COLOR': `"${DARK_MODE_DIMMED_WARNING_COLOR}"`,
84 - 'process.env.DARK_MODE_DIMMED_ERROR_COLOR': `"${DARK_MODE_DIMMED_ERROR_COLOR}"`,
85 - 'process.env.DARK_MODE_DIMMED_LOG_COLOR': `"${DARK_MODE_DIMMED_LOG_COLOR}"`,
86 - 'process.env.LIGHT_MODE_DIMMED_WARNING_COLOR': `"${LIGHT_MODE_DIMMED_WARNING_COLOR}"`,
87 - 'process.env.LIGHT_MODE_DIMMED_ERROR_COLOR': `"${LIGHT_MODE_DIMMED_ERROR_COLOR}"`,
88 - 'process.env.LIGHT_MODE_DIMMED_LOG_COLOR': `"${LIGHT_MODE_DIMMED_LOG_COLOR}"`,
89 - }),
90 - ],
91 - module: {
92 - rules: [
170 +);
171 +
172 +// Prior to React 18, we use ReactDOM.render rather than
173 +// createRoot.
174 +// We also use a separate build folder to build the React App
175 +// so that we can test the current DevTools against older version of React
176 +const e2eRegressionApp = semver.lt(REACT_VERSION, '18.0.0')
177 + ? makeConfig(
178 {
94 - test: /\.js$/,
95 - loader: 'babel-loader',
96 - options: {
97 - configFile: resolve(
98 - __dirname,
99 - '..',
100 - 'react-devtools-shared',
101 - 'babel.config.js',
102 - ),
103 - },
179 + 'e2e-app-regression': './src/e2e-regression/app-legacy.js',
180 },
181 {
106 - test: /\.css$/,
107 - use: [
108 - {
109 - loader: 'style-loader',
110 - },
111 - {
112 - loader: 'css-loader',
113 - options: {
114 - sourceMap: true,
115 - modules: true,
116 - localIdentName: '[local]',
117 - },
118 - },
119 - ],
182 + react: resolve(E2E_APP_BUILD_DIR, 'react'),
183 + 'react-dom': resolve(E2E_APP_BUILD_DIR, 'react-dom'),
184 + ...(semver.satisfies(REACT_VERSION, '16.5')
185 + ? {schedule: resolve(E2E_APP_BUILD_DIR, 'schedule')}
186 + : {scheduler: resolve(E2E_APP_BUILD_DIR, 'scheduler')}),
187 },
121 - ],
122 - },
123 -};
124 -
125 -if (TARGET === 'local') {
126 - // Local dev server build.
127 - config.devServer = {
128 - hot: true,
129 - port: 8080,
130 - clientLogLevel: 'warning',
131 - publicPath: '/dist/',
132 - stats: 'errors-only',
133 - };
134 -} else {
135 - // Static build to deploy somewhere else.
136 - config.output = {
137 - path: resolve(__dirname, 'dist'),
138 - filename: '[name].js',
139 - };
140 -}
188 + )
189 + : makeConfig(
190 + {
191 + 'e2e-app-regression': './src/e2e-regression/app.js',
192 + },
193 + {
194 + react: resolve(E2E_APP_BUILD_DIR, 'react'),
195 + 'react-dom': resolve(E2E_APP_BUILD_DIR, 'react-dom'),
196 + 'react-dom/client': resolve(E2E_APP_BUILD_DIR, 'react-dom'),
197 + scheduler: resolve(E2E_APP_BUILD_DIR, 'scheduler'),
198 + },
199 + );
200
142 -module.exports = config;
201 +module.exports = [app, e2eRegressionApp];