Skip to content

Commit

Permalink
Merge pull request #186 from kirovboris/react_18
Browse files Browse the repository at this point in the history
feat: support react 18
  • Loading branch information
kirovboris authored May 13, 2022
2 parents a77ca8c + 710de2c commit 6f4b03d
Show file tree
Hide file tree
Showing 39 changed files with 843 additions and 1,058 deletions.
2 changes: 1 addition & 1 deletion .appveyor.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
clone_depth: 1

environment:
NODEJS_VERSION: "12"
NODEJS_VERSION: "14"

install:
- ps: >-
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ lib/*
test/data/lib/*
test/data/server-render/.next
package-lock.json
yarn.lock
74 changes: 40 additions & 34 deletions Gulpfile.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
const babel = require('gulp-babel');
const createTestCafe = require('testcafe');
const del = require('del');
const eslint = require('gulp-eslint-new');
const fs = require('fs');
const glob = require('glob');
const gulp = require('gulp');
const mustache = require('gulp-mustache');
const pathJoin = require('path').join;
const rename = require('gulp-rename');
const startTestServer = require('./test/server');
const { promisify } = require('util');
const nextBuild = require('next/dist/build').default;
const createTestCafe = require('testcafe');
const del = require('del');
const eslint = require('gulp-eslint-new');
const fs = require('fs');
const glob = require('glob');
const gulp = require('gulp');
const mustache = require('gulp-mustache');
const pathJoin = require('path').join;
const rename = require('gulp-rename');
const startTestServer = require('./test/server');
const { promisify } = require('util');
const nextBuild = require('next/dist/build').default;
const { createServer } = require('vite');

const listFiles = promisify(glob);
const deleteFiles = promisify(del);

let devServer = null;

gulp.task('clean', () => {
return deleteFiles([
'lib',
Expand All @@ -35,13 +37,6 @@ gulp.task('lint', () => {
.pipe(eslint.failAfterError());
});

gulp.task('build-test-app', () => {
return gulp
.src('test/data/src/**/*.{jsx,js}')
.pipe(babel())
.pipe(gulp.dest('test/data/lib'));
});

gulp.task('build-selectors-script', () => {
function loadModule (modulePath) {
return fs.readFileSync(modulePath).toString();
Expand All @@ -50,24 +45,17 @@ gulp.task('build-selectors-script', () => {
return gulp.src('./src/index.js.mustache')
.pipe(mustache({
getRootElsReact15: loadModule('./src/react-15/get-root-els.js'),
getRootElsReact16or17: loadModule('./src/react-16-17/get-root-els.js'),
getRootElsReact16to18: loadModule('./src/react-16-18/get-root-els.js'),

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

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

waitForReact: loadModule('./src/wait-for-react.js')
}))
.pipe(rename('index.js'))
.pipe(gulp.dest('lib/tmp'));
});

gulp.task('transpile', () => {
return gulp
.src('lib/tmp/**/*.js')
.pipe(babel({ extends: pathJoin(__dirname, './src/.babelrc') }))
.pipe(gulp.dest('lib'));
});

Expand All @@ -81,7 +69,23 @@ gulp.task('build-nextjs-app', () => {
return nextBuild(appPath, require('./next.config.js'));
});

gulp.task('build', gulp.series('clean', 'lint', 'build-selectors-script', 'transpile', 'clean-build-tmp-resources'));
gulp.task('build', gulp.series('clean', 'lint', 'build-selectors-script', 'clean-build-tmp-resources'));

gulp.task('start-dev-server', async () => {
const src = 'test/data/app';

devServer = await createServer({
configFile: false,
root: src,

server: {
port: 3000
}
});

await devServer.listen();
});


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

await testCafe.createRunner()
.src(files)
.browsers(['chrome', 'firefox', 'ie'])
.browsers(['chrome', 'firefox', 'edge'])
.reporter('list')
.run({ quarantineMode: true })
.run({ quarantineMode: true, debugOnFail: false })
.then(failed => {
devServer.close();

cb();
process.exit(failed);
});
});

gulp.task('test', gulp.series('build', 'build-test-app', 'build-nextjs-app', 'run-tests'));
gulp.task('test', gulp.series('build', 'start-dev-server', 'build-nextjs-app', 'run-tests'));
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ export type OptionReactComponent = ReactComponent<Props, State>;

### Limitations

* `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.
* `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.
* Search for a component starts from the root React component, so selectors like `ReactSelector('body MyComponent')` will return `null`.
* 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.

Expand Down
21 changes: 10 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "testcafe-react-selectors",
"version": "4.1.5",
"version": "5.0.0",
"description": "ReactJS selectors for TestCafe",
"repository": "https://github.com/DevExpress/testcafe-react-selectors",
"main": "lib/index",
Expand All @@ -14,26 +14,25 @@
},
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.17.8",
"@babel/eslint-parser": "^7.17.0",
"@babel/plugin-transform-for-of": "^7.16.7",
"@babel/plugin-transform-runtime": "^7.17.0",
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"chai": "^3.2.0",
"@babel/eslint-parser": "^7.17.0",
"del": "^1.2.0",
"express": "^4.16.3",
"glob": "^7.1.7",
"gulp": "^4.0.2",
"gulp-babel": "8.0.0",
"gulp-eslint-new": "^1.4.2",
"gulp-mustache": "^3.0.1",
"gulp-rename": "^1.2.2",
"next": "^12.1.0",
"publish-please": "^5.5.2",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"testcafe": "^1.15.2"
"react": "npm:react@^18.0.0",
"react-dom": "npm:react-dom@^18.0.0",
"react-dom16": "npm:react-dom@^16.0.0",
"react-dom17": "npm:react-dom@^17.0.0",
"react16": "npm:react@^16.0.0",
"react17": "npm:react@^17.0.0",
"testcafe": "^1.18.6",
"vite": "^2.9.6"
},
"scripts": {
"test": "set NEXT_TELEMETRY_DISABLED=1 && gulp test",
Expand Down
33 changes: 17 additions & 16 deletions src/index.js.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { Selector, ClientFunction } from 'testcafe';

export const ReactSelector = Selector(selector => {
const getRootElsReact15 = {{{getRootElsReact15}}}
const getRootElsReact16or17 = {{{getRootElsReact16or17}}}
const getRootElsReact16to18 = {{{getRootElsReact16to18}}}
const selectorReact15 = {{{selectorReact15}}}
const selectorReact16or17 = {{{selectorReact16or17}}}
const selectorReact16to18 = {{{selectorReact16to18}}}

let visitedRootEls = [];
let rootEls = null;
Expand All @@ -26,12 +26,12 @@ export const ReactSelector = Selector(selector => {
}

const react15Utils = {{{react15Utils}}}
const react16or17Utils = {{{react16or17Utils}}}
const react16to18Utils = {{{react16to18Utils}}}

if(!window['%testCafeReactSelectorUtils%']) {
window['%testCafeReactSelectorUtils%'] = {
'15' : react15Utils,
'16|17': react16or17Utils
'16|17|18': react16to18Utils
};
}

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

rootEls = getRootElsReact16or17();
rootEls = getRootElsReact16to18();

if(rootEls.length) {
const rootContainers = rootEls.map(root => root.return);
//NOTE: root.return for 16 and 17 version
const rootContainers = rootEls.map(root => root.return || root);
window['%testCafeReactVersion%'] = '16|17';
window['$testCafeReactSelector'] = selectorReact16or17;
window['$testCafeReact16or17Roots'] = rootEls;
window['$testCafeReact16or17RootContainers'] = rootContainers;
window['%testCafeReactVersion%'] = '16|17|18';
window['$testCafeReactSelector'] = selectorReact16to18;
window['$testCafeReact16to18Roots'] = rootEls;
window['$testCafeReact16to18RootContainers'] = rootContainers;
foundDOMNodes = selectorReact16or17(selector, false);
foundDOMNodes = selectorReact16to18(selector, false);
}

visitedRootEls = [];

if(foundDOMNodes)
return foundDOMNodes;

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.");
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.");
}).addCustomMethods({
getReact: (node, fn) => {
const reactVersion = window['%testCafeReactVersion%'];
const reactUtils = window['%testCafeReactSelectorUtils%'][reactVersion];
delete window['%testCafeReactVersion%'];
return reactUtils.getReact(node, fn);
}
}).addCustomMethods({
Expand Down Expand Up @@ -127,9 +128,9 @@ export const ReactSelector = Selector(selector => {
}

return true;
}
}

const reactVersion = window['%testCafeReactVersion%'];
const reactVersion = window['%testCafeReactVersion%'];
let filterProps = {};
let options = null;

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

findReact: (nodes, selector) => {
const reactVersion = window['%testCafeReactVersion%'];
const reactUtils = window['%testCafeReactSelectorUtils%'][reactVersion];
const reactUtils = window['%testCafeReactSelectorUtils%'][reactVersion];
let componentInstances = null;
let scanDOMNodeForReactComponent = reactUtils.scanDOMNodeForReactComponent;
Expand Down
24 changes: 0 additions & 24 deletions src/react-16-17/get-root-els.js

This file was deleted.

39 changes: 39 additions & 0 deletions src/react-16-18/get-root-els.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*global document*/

/*eslint-disable no-unused-vars*/
function getRootElsReact16to18 (el) {
el = el || document.body;

let rootEls = [];

if (el._reactRootContainer) {
const rootContainer = el._reactRootContainer._internalRoot || el._reactRootContainer;

rootEls.push(rootContainer.current.child);
}

else {
//NOTE: approach for React 18 createRoot API
for (var prop of Object.keys(el)) {
if (!/^__reactContainer/.test(prop)) continue;

//NOTE: component and its alternate version has the same stateNode, but stateNode has the link to rendered version in the 'current' field
const component = el[prop].stateNode.current;

rootEls.push(component);

break;
}
}

const children = el.children;

for (let index = 0; index < children.length; ++index) {
const child = children[index];

rootEls = rootEls.concat(getRootElsReact16to18(child));

}

return rootEls;
}
14 changes: 8 additions & 6 deletions src/react-16-17/index.js → src/react-16-18/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/*global window document Node rootEls defineSelectorProperty visitedRootEls checkRootNodeVisited*/

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

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

/*eslint-enable no-unused-vars*/
function createAnnotationForEmptyComponent (component) {
Expand All @@ -23,16 +23,18 @@ function react16or17Selector (selector, renderedRootIsUnknown, parents = rootEls
//react memo
// it will find the displayName on the elementType if you set it
if (component.elementType && component.elementType.displayName) return component.elementType.displayName;


if (!component.type && !component.memoizedState)
return null;

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

//NOTE: tag
if (typeof component.type === 'string') return component.type;
if (component.type.displayName || component.type.name) return component.type.displayName || component.type.name;
if (component.type) {
if (typeof component.type === 'string') return component.type;
if (component.type.displayName || component.type.name) return component.type.displayName || component.type.name;
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
}

function getRenderedComponentVersion (component) {
const rootContainers = window['$testCafeReact16or17RootContainers'];
const rootContainers = window['$testCafeReact16to18RootContainers'];

if (!component.alternate) return component;

Expand All @@ -109,7 +109,7 @@
}

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

return getRenderedComponentVersion(reactInstance);
Expand Down
Loading

0 comments on commit 6f4b03d

Please sign in to comment.