Skip to content

Commit a6ee050

Browse files
committed
[WIP] feat: support react 18
1 parent a77ca8c commit a6ee050

38 files changed

+7822
-1043
lines changed

Gulpfile.js

+42-28
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
1-
const babel = require('gulp-babel');
2-
const createTestCafe = require('testcafe');
3-
const del = require('del');
4-
const eslint = require('gulp-eslint-new');
5-
const fs = require('fs');
6-
const glob = require('glob');
7-
const gulp = require('gulp');
8-
const mustache = require('gulp-mustache');
9-
const pathJoin = require('path').join;
10-
const rename = require('gulp-rename');
11-
const startTestServer = require('./test/server');
12-
const { promisify } = require('util');
13-
const nextBuild = require('next/dist/build').default;
1+
const babel = require('gulp-babel');
2+
const createTestCafe = require('testcafe');
3+
const del = require('del');
4+
const eslint = require('gulp-eslint-new');
5+
const fs = require('fs');
6+
const glob = require('glob');
7+
const gulp = require('gulp');
8+
const mustache = require('gulp-mustache');
9+
const pathJoin = require('path').join;
10+
const rename = require('gulp-rename');
11+
const startTestServer = require('./test/server');
12+
const { promisify } = require('util');
13+
const nextBuild = require('next/dist/build').default;
14+
const { createServer } = require('vite');
1415

1516
const listFiles = promisify(glob);
1617
const deleteFiles = promisify(del);
1718

19+
let devServer = null;
20+
1821
gulp.task('clean', () => {
1922
return deleteFiles([
2023
'lib',
@@ -35,13 +38,6 @@ gulp.task('lint', () => {
3538
.pipe(eslint.failAfterError());
3639
});
3740

38-
gulp.task('build-test-app', () => {
39-
return gulp
40-
.src('test/data/src/**/*.{jsx,js}')
41-
.pipe(babel())
42-
.pipe(gulp.dest('test/data/lib'));
43-
});
44-
4541
gulp.task('build-selectors-script', () => {
4642
function loadModule (modulePath) {
4743
return fs.readFileSync(modulePath).toString();
@@ -50,18 +46,18 @@ gulp.task('build-selectors-script', () => {
5046
return gulp.src('./src/index.js.mustache')
5147
.pipe(mustache({
5248
getRootElsReact15: loadModule('./src/react-15/get-root-els.js'),
53-
getRootElsReact16or17: loadModule('./src/react-16-17/get-root-els.js'),
49+
getRootElsReact16to18: loadModule('./src/react-16-18/get-root-els.js'),
5450

5551
selectorReact15: loadModule('./src/react-15/index.js'),
56-
selectorReact16or17: loadModule('./src/react-16-17/index.js'),
52+
selectorReact16to18: loadModule('./src/react-16-18/index.js'),
5753

5854
react15Utils: loadModule('./src/react-15/react-utils.js'),
59-
react16or17Utils: loadModule('./src/react-16-17/react-utils.js'),
55+
react16to18Utils: loadModule('./src/react-16-18/react-utils.js'),
6056

6157
waitForReact: loadModule('./src/wait-for-react.js')
6258
}))
6359
.pipe(rename('index.js'))
64-
.pipe(gulp.dest('lib/tmp'));
60+
.pipe(gulp.dest('lib'));
6561
});
6662

6763
gulp.task('transpile', () => {
@@ -81,7 +77,23 @@ gulp.task('build-nextjs-app', () => {
8177
return nextBuild(appPath, require('./next.config.js'));
8278
});
8379

84-
gulp.task('build', gulp.series('clean', 'lint', 'build-selectors-script', 'transpile', 'clean-build-tmp-resources'));
80+
gulp.task('build', gulp.series('clean', 'lint', 'build-selectors-script', /* 'transpile',*/ 'clean-build-tmp-resources'));
81+
82+
gulp.task('start-dev-server', async () => {
83+
const src = 'test/data/app';
84+
85+
devServer = await createServer({
86+
configFile: false,
87+
root: src,
88+
89+
server: {
90+
port: 3000
91+
}
92+
});
93+
94+
await devServer.listen();
95+
});
96+
8597

8698
gulp.task('run-tests', async cb => {
8799
const files = await listFiles('test/fixtures/**/*.{js,ts}');
@@ -92,13 +104,15 @@ gulp.task('run-tests', async cb => {
92104

93105
await testCafe.createRunner()
94106
.src(files)
95-
.browsers(['chrome', 'firefox', 'ie'])
107+
.browsers(['chrome'/*, 'firefox', 'ie'*/])
96108
.reporter('list')
97-
.run({ quarantineMode: true })
109+
.run({ quarantineMode: false, debugOnFail: true })
98110
.then(failed => {
111+
devServer.close();
112+
99113
cb();
100114
process.exit(failed);
101115
});
102116
});
103117

104-
gulp.task('test', gulp.series('build', 'build-test-app', 'build-nextjs-app', 'run-tests'));
118+
gulp.task('test', gulp.series('build', 'start-dev-server', 'build-nextjs-app', 'run-tests'));

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ export type OptionReactComponent = ReactComponent<Props, State>;
451451

452452
### Limitations
453453

454-
* `testcafe-react-selectors` support ReactJS starting with version 15. To check if a component can be found, use the [react-dev-tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi) extension.
454+
* `testcafe-react-selectors` support ReactJS starting with version 16. To check if a component can be found, use the [react-dev-tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi) extension.
455455
* Search for a component starts from the root React component, so selectors like `ReactSelector('body MyComponent')` will return `null`.
456456
* ReactSelectors need class names to select components on the page. Code minification usually does not keep the original class names. So you should either use non-minified code or configure the minificator to keep class names.
457457

package.json

+8-3
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,14 @@
3131
"gulp-rename": "^1.2.2",
3232
"next": "^12.1.0",
3333
"publish-please": "^5.5.2",
34-
"react": "^17.0.1",
35-
"react-dom": "^17.0.1",
36-
"testcafe": "^1.15.2"
34+
"react": "npm:react@^18.0.0",
35+
"react-dom": "npm:react-dom@^18.0.0",
36+
"react-dom16": "npm:react-dom@^16.0.0",
37+
"react-dom17": "npm:react-dom@^17.0.0",
38+
"react16": "npm:react@^16.0.0",
39+
"react17": "npm:react@^17.0.0",
40+
"testcafe": "^1.18.6",
41+
"vite": "^2.9.6"
3742
},
3843
"scripts": {
3944
"test": "set NEXT_TELEMETRY_DISABLED=1 && gulp test",

src/index.js.mustache

+17-16
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { Selector, ClientFunction } from 'testcafe';
33

44
export const ReactSelector = Selector(selector => {
55
const getRootElsReact15 = {{{getRootElsReact15}}}
6-
const getRootElsReact16or17 = {{{getRootElsReact16or17}}}
6+
const getRootElsReact16to18 = {{{getRootElsReact16to18}}}
77
const selectorReact15 = {{{selectorReact15}}}
8-
const selectorReact16or17 = {{{selectorReact16or17}}}
8+
const selectorReact16to18 = {{{selectorReact16to18}}}
99

1010
let visitedRootEls = [];
1111
let rootEls = null;
@@ -26,12 +26,12 @@ export const ReactSelector = Selector(selector => {
2626
}
2727

2828
const react15Utils = {{{react15Utils}}}
29-
const react16or17Utils = {{{react16or17Utils}}}
29+
const react16to18Utils = {{{react16to18Utils}}}
3030

3131
if(!window['%testCafeReactSelectorUtils%']) {
3232
window['%testCafeReactSelectorUtils%'] = {
3333
'15' : react15Utils,
34-
'16|17': react16or17Utils
34+
'16|17|18': react16to18Utils
3535
};
3636
}
3737

@@ -46,33 +46,34 @@ export const ReactSelector = Selector(selector => {
4646
foundDOMNodes = selectorReact15(selector);
4747
}
4848

49-
rootEls = getRootElsReact16or17();
49+
rootEls = getRootElsReact16to18();
5050

5151
if(rootEls.length) {
52-
const rootContainers = rootEls.map(root => root.return);
52+
//NOTE: root.return for 16 and 17 version
53+
const rootContainers = rootEls.map(root => root.return || root);
5354
54-
window['%testCafeReactVersion%'] = '16|17';
55-
window['$testCafeReactSelector'] = selectorReact16or17;
56-
window['$testCafeReact16or17Roots'] = rootEls;
57-
window['$testCafeReact16or17RootContainers'] = rootContainers;
55+
window['%testCafeReactVersion%'] = '16|17|18';
56+
window['$testCafeReactSelector'] = selectorReact16to18;
57+
window['$testCafeReact16to18Roots'] = rootEls;
58+
window['$testCafeReact16to18RootContainers'] = rootContainers;
5859
5960
60-
foundDOMNodes = selectorReact16or17(selector, false);
61+
foundDOMNodes = selectorReact16to18(selector, false);
6162
}
6263

6364
visitedRootEls = [];
6465

6566
if(foundDOMNodes)
6667
return foundDOMNodes;
6768

68-
throw new Error("React component tree is not loaded yet or the current React version is not supported. This module supports React version 15.x and newer. To wait until the React's component tree is loaded, add the `waitForReact` method to fixture's `beforeEach` hook.");
69+
throw new Error("React component tree is not loaded yet or the current React version is not supported. This module supports React version 16.x and newer. To wait until the React's component tree is loaded, add the `waitForReact` method to fixture's `beforeEach` hook.");
6970
}).addCustomMethods({
7071
getReact: (node, fn) => {
7172
const reactVersion = window['%testCafeReactVersion%'];
7273
const reactUtils = window['%testCafeReactSelectorUtils%'][reactVersion];
7374
7475
delete window['%testCafeReactVersion%'];
75-
76+
7677
return reactUtils.getReact(node, fn);
7778
}
7879
}).addCustomMethods({
@@ -127,9 +128,9 @@ export const ReactSelector = Selector(selector => {
127128
}
128129

129130
return true;
130-
}
131+
}
131132

132-
const reactVersion = window['%testCafeReactVersion%'];
133+
const reactVersion = window['%testCafeReactVersion%'];
133134
let filterProps = {};
134135
let options = null;
135136

@@ -209,7 +210,7 @@ export const ReactSelector = Selector(selector => {
209210

210211
findReact: (nodes, selector) => {
211212
const reactVersion = window['%testCafeReactVersion%'];
212-
const reactUtils = window['%testCafeReactSelectorUtils%'][reactVersion];
213+
const reactUtils = window['%testCafeReactSelectorUtils%'][reactVersion];
213214
214215
let componentInstances = null;
215216
let scanDOMNodeForReactComponent = reactUtils.scanDOMNodeForReactComponent;

src/react-16-17/get-root-els.js

-24
This file was deleted.

src/react-16-18/get-root-els.js

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*global document*/
2+
3+
/*eslint-disable no-unused-vars*/
4+
function getRootElsReact16to18 (el) {
5+
el = el || document.body;
6+
7+
let rootEls = [];
8+
9+
if (el._reactRootContainer) {
10+
const rootContainer = el._reactRootContainer._internalRoot || el._reactRootContainer;
11+
12+
rootEls.push(rootContainer.current.child);
13+
}
14+
15+
else {
16+
//NOTE: approach for React 18 createRoot API
17+
for (var prop of Object.keys(el)) {
18+
if (!/^__reactContainer/.test(prop)) continue;
19+
20+
//NOTE: component and its alternate version has the same stateNode, but stateNode has the link to rendered version in the 'current' field
21+
const component = el[prop].stateNode.current;
22+
23+
rootEls.push(component);
24+
25+
break;
26+
}
27+
}
28+
29+
const children = el.children;
30+
31+
for (let index = 0; index < children.length; ++index) {
32+
const child = children[index];
33+
34+
rootEls = rootEls.concat(getRootElsReact16to18(child));
35+
36+
}
37+
38+
return rootEls;
39+
}

src/react-16-17/index.js src/react-16-18/index.js

+11-6
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
/*global window document Node rootEls defineSelectorProperty visitedRootEls checkRootNodeVisited*/
22

33
/*eslint-disable no-unused-vars*/
4-
function react16or17Selector (selector, renderedRootIsUnknown, parents = rootEls) {
4+
function react16to18Selector (selector, renderedRootIsUnknown, parents = rootEls) {
55
window['%testCafeReactFoundComponents%'] = [];
66

7-
const { getRenderedComponentVersion } = window['%testCafeReactSelectorUtils%']['16|17'];
7+
const { getRenderedComponentVersion } = window['%testCafeReactSelectorUtils%']['16|17|18'];
88

99
/*eslint-enable no-unused-vars*/
1010
function createAnnotationForEmptyComponent (component) {
@@ -23,16 +23,21 @@ function react16or17Selector (selector, renderedRootIsUnknown, parents = rootEls
2323
//react memo
2424
// it will find the displayName on the elementType if you set it
2525
if (component.elementType && component.elementType.displayName) return component.elementType.displayName;
26-
27-
26+
27+
2828
if (!component.type && !component.memoizedState)
2929
return null;
3030

3131
const currentElement = component.type ? component : component.memoizedState.element;
3232

33+
//NOTE: React.StrictMode
34+
// if (currentElement && typeof currentElement.type === 'symbol') return 'React_Service_Component';
35+
3336
//NOTE: tag
34-
if (typeof component.type === 'string') return component.type;
35-
if (component.type.displayName || component.type.name) return component.type.displayName || component.type.name;
37+
if (component.type) {
38+
if (typeof component.type === 'string') return component.type;
39+
if (component.type.displayName || component.type.name) return component.type.displayName || component.type.name;
40+
}
3641

3742
const matches = currentElement.type.toString().match(/^function\s*([^\s(]+)/);
3843

src/react-16-17/react-utils.js src/react-16-18/react-utils.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
}
9494

9595
function getRenderedComponentVersion (component) {
96-
const rootContainers = window['$testCafeReact16or17RootContainers'];
96+
const rootContainers = window['$testCafeReact16to18RootContainers'];
9797

9898
if (!component.alternate) return component;
9999

@@ -109,7 +109,7 @@
109109
}
110110

111111
function scanDOMNodeForReactComponent (domNode) {
112-
const rootInstances = window['$testCafeReact16or17Roots'].map(rootEl => rootEl.return || rootEl);
112+
const rootInstances = window['$testCafeReact16to18Roots'].map(rootEl => rootEl.return || rootEl);
113113
const reactInstance = scanDOMNodeForReactInstance(domNode);
114114

115115
return getRenderedComponentVersion(reactInstance);

src/wait-for-react.js

+10-4
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,18 @@ function waitForReact (timeout, testController) {
1010
const CHECK_INTERVAL = 200;
1111
let stopChecking = false;
1212

13-
function findReact16or17Root () {
13+
function findReact16to18Root () {
1414
const treeWalker = document.createTreeWalker(document, NodeFilter.SHOW_ELEMENT, null, false);
1515

16-
while (treeWalker.nextNode())
16+
while (treeWalker.nextNode()) {
17+
//NOTE: fast check for 16 and 17 react
1718
if (treeWalker.currentNode.hasOwnProperty('_reactRootContainer')) return true;
1819

20+
//NOTE: react 18
21+
for (const prop of Object.keys(treeWalker.currentNode))
22+
if (/^__reactContainer/.test(prop)) return true;
23+
}
24+
1925
return false;
2026
}
2127

@@ -28,9 +34,9 @@ function waitForReact (timeout, testController) {
2834

2935
function findReactApp () {
3036
const isReact15OrStaticRender = findReact15OrStaticRenderedRoot();
31-
const isReact16or17WithHandlers = !!Object.keys(document).filter(prop => /^_reactListenersID|^_reactEvents/.test(prop)).length;
37+
const isReact16to18WithHandlers = !!Object.keys(document).filter(prop => /^_reactListenersID|^_reactEvents/.test(prop)).length;
3238

33-
return isReact15OrStaticRender || isReact16or17WithHandlers || findReact16or17Root();
39+
return isReact15OrStaticRender || isReact16to18WithHandlers || findReact16to18Root();
3440
}
3541

3642
return new Promise((resolve, reject) => {

0 commit comments

Comments
 (0)