diff --git a/common/compilers.js b/common/compilers.js index 862216e..eb0ab69 100644 --- a/common/compilers.js +++ b/common/compilers.js @@ -3,13 +3,15 @@ const { COMPILER_OPTS_MAPPING } = require('./constants') -exports.remove = function removeCompilers (j, root) { +const { isStringLiteral } = require('./utils') + +exports.remove = function removeCompilers (j, root, opts) { const autoCompileOpts = {} /** * remove compiler requires */ - root.find(j.Property) + root.find(opts.parser === 'babel' ? j.Property : j.ObjectProperty) .filter((path) => ( path.value.key && ( path.value.key.name === 'require' || @@ -23,7 +25,7 @@ exports.remove = function removeCompilers (j, root) { j.arrayExpression(path.value.value.elements.filter((value) => { let importName - if (value.type === 'Literal') { + if (isStringLiteral(value)) { importName = value.value } else if (value.type === 'ArrayExpression') { importName = value.elements[0].value @@ -57,8 +59,7 @@ exports.remove = function removeCompilers (j, root) { }) root.find(j.ExpressionStatement, { expression: { callee: { - callee: { name: 'require' }, - arguments: [{ type: 'Literal' }] + callee: { name: 'require' } } } }).filter((path) => ( Object.keys(COMPILER_OPTS_MAPPING).includes(path.value.expression.callee.arguments[0].value) @@ -87,26 +88,24 @@ exports.remove = function removeCompilers (j, root) { return autoCompileOpts } -exports.update = function (j, root, autoCompileOpts) { +exports.update = function (j, root, autoCompileOpts, opts) { /** * update config with compiler opts */ - let wasReplaced = false - root.find(j.Property) + let wasInserted = false + root.find(opts.parser === 'babel' ? j.Property : j.ObjectProperty) .filter((path) => ( path.value.key && ( path.value.key.name === 'capabilities' || path.value.key.name === 'framework' ) )) - .replaceWith((path) => { - if (wasReplaced) { - return path.value + .forEach((path) => { + if (wasInserted) { + return } - - wasReplaced = true - return [ - path.value, + wasInserted = true + path.parentPath.value.push( ...(Object.keys(autoCompileOpts).length ? [j.objectProperty( j.identifier('autoCompileOpts'), @@ -125,6 +124,6 @@ exports.update = function (j, root, autoCompileOpts) { )] : [] ) - ] + ) }) } diff --git a/common/utils.js b/common/utils.js new file mode 100644 index 0000000..b77efe2 --- /dev/null +++ b/common/utils.js @@ -0,0 +1,12 @@ +function isStringLiteral (val) { + return ['Literal', 'StringLiteral'].includes(val.type) +} + +function isNumericalLiteral (val) { + return ['Literal', 'NumericLiteral'].includes(val.type) +} + +module.exports = { + isStringLiteral, + isNumericalLiteral +} diff --git a/protractor/index.js b/protractor/index.js index c7ae39b..9440e2a 100644 --- a/protractor/index.js +++ b/protractor/index.js @@ -26,10 +26,11 @@ const { replaceCommands, parseConfigProperties, sanitizeAsyncCalls, + failAsyncConstructor, makeAsync } = require('./utils') -module.exports = function transformer(file, api) { +module.exports = function transformer(file, api, opts) { const j = api.jscodeshift; const root = j(file.source); j.file = file @@ -91,7 +92,7 @@ module.exports = function transformer(file, api) { ) }) - const autoCompileOpts = compilers.remove(j, root) + const autoCompileOpts = compilers.remove(j, root, opts) /** * remove all protractor import declarations @@ -253,7 +254,8 @@ module.exports = function transformer(file, api) { }) .replaceWith((path) => j.memberExpression( path.value.callee.object, - path.value.arguments[0] + path.value.arguments[0], + true )) /** @@ -570,7 +572,8 @@ module.exports = function transformer(file, api) { j.memberExpression( j.memberExpression( path.value.callee.object.callee.object, - path.value.callee.object.arguments[0] + path.value.callee.object.arguments[0], + true ), j.identifier(replaceCommands(command)) ), @@ -761,7 +764,7 @@ module.exports = function transformer(file, api) { * transform element declarations in class constructors into getters */ const elementGetters = new Map() - root.find(j.MethodDefinition, { kind: 'constructor' }).replaceWith((path) => { + const setGetters = (path) => { const isElementDeclaration = (e) => ( e.expression && e.expression.type === 'AssignmentExpression' && e.expression.left.object && e.expression.left.object.type === 'ThisExpression' && @@ -772,20 +775,16 @@ module.exports = function transformer(file, api) { ) ) - for (const e of path.value.value.body.body.filter(isElementDeclaration)) { + const body = path.value.body || path.value.value.body + const kind = path.value.kind || path.value.value.kind + const key = path.value.key || path.value.value.key + const params = path.value.params || path.value.value.params + for (const e of body.body.filter(isElementDeclaration)) { elementGetters.set(e.expression.left.property, e.expression.right) } return [ - j.methodDefinition( - path.value.kind, - path.value.key, - j.functionExpression( - path.value.value.id, - path.value.value.params, - j.blockStatement(path.value.value.body.body.filter((e) => !isElementDeclaration(e))) - ) - ), + j.classMethod(kind, key, params, j.blockStatement(body.body.filter((e) => !isElementDeclaration(e)))), ...[...elementGetters.entries()].map(([elemName, object]) => j.methodDefinition( 'get', elemName, @@ -798,7 +797,9 @@ module.exports = function transformer(file, api) { ) )) ] - }) + } + root.find(j.ClassMethod, { key: { name: 'constructor' } }).replaceWith(setGetters) + root.find(j.MethodDefinition, { key: { name: 'constructor' } }).replaceWith(setGetters) /** * transform lazy loaded element calls in async context, e.g. @@ -849,21 +850,14 @@ module.exports = function transformer(file, api) { )).replaceWith((path) => { j(path).closest(j.FunctionExpression).replaceWith(makeAsync) j(path).closest(j.ArrowFunctionExpression).replaceWith(makeAsync) - j(path).closest(j.MethodDefinition, { - key: { name: 'constructor' } - }).forEach((p) => { - throw new TransformError('' + - `With "this.${path.value.property.name}" you are ` + - 'trying to access an element within a constructor. Given that it ' + - 'is not possible to run asynchronous code in this context, it ' + - 'is advised to move this call into a method or getter function.', - path.value, - file - ) - }) + j(path).closest(j.ClassMethod).replaceWith(makeAsync) + const constructorFilter = { key: { name: 'constructor' } } + const throwConstructorError = () => failAsyncConstructor(path, file) + j(path).closest(j.MethodDefinition, constructorFilter).forEach(throwConstructorError) + j(path).closest(j.ClassMethod, constructorFilter).forEach(throwConstructorError) return j.awaitExpression(path.value) }) - compilers.update(j, root, autoCompileOpts) + compilers.update(j, root, autoCompileOpts, opts) return root.toSource() } diff --git a/protractor/utils.js b/protractor/utils.js index ad4cb56..f576859 100644 --- a/protractor/utils.js +++ b/protractor/utils.js @@ -1,6 +1,11 @@ const url = require('url') const { format } = require('util') +const { + isStringLiteral, + isNumericalLiteral +} = require('../common/utils') + const { IGNORED_CONFIG_PROPERTIES, UNSUPPORTED_CONFIG_OPTION_ERROR, @@ -45,7 +50,7 @@ function getSelectorArgument (j, path, callExpr, file) { file ) } else if (bySelector === 'id') { - return [arg.type === 'Literal' + return [isStringLiteral(arg) ? j.literal(`#${arg.value}`) : j.templateLiteral([ j.templateElement({ raw: '#', cooked: '#' }, false) @@ -54,7 +59,7 @@ function getSelectorArgument (j, path, callExpr, file) { ]) ] } else if (bySelector === 'model') { - return [arg.type === 'Literal' + return [isStringLiteral(arg) ? j.literal(`*[ng-model="${arg.value}"]`) : j.templateLiteral([ j.templateElement({ raw: '*[ng-model="', cooked: '*[ng-model="' }, false), @@ -64,7 +69,7 @@ function getSelectorArgument (j, path, callExpr, file) { ]) ] } else if (bySelector === 'repeater') { - return [arg.type === 'Literal' + return [isStringLiteral(arg) ? j.literal(`*[ng-repeat="${arg.value}"]`) : j.templateLiteral([ j.templateElement({ raw: '*[ng-repeat="', cooked: '*[ng-repeat="' }, false), @@ -80,7 +85,7 @@ function getSelectorArgument (j, path, callExpr, file) { if (text.regex) { throw new TransformError('this codemod does not support RegExp in cssContainingText', path.value, file) - } else if (text.type === 'Literal') { + } else if (isStringLiteral(text)) { return [j.literal(`${arg.value}=${text.value}`)] } else if (text.type === 'Identifier') { return [ @@ -96,21 +101,21 @@ function getSelectorArgument (j, path, callExpr, file) { } else if (bySelector === 'xpath' || bySelector === 'tagName' || bySelector === 'js') { return [arg] } else if (bySelector === 'linkText') { - return [arg.type === 'Literal' + return [isStringLiteral(arg) ? j.literal(`=${arg.value}`) : j.templateLiteral([ j.templateElement({ raw: '=', cooked: '=' }, false) ], [arg]) ] } else if (bySelector === 'partialLinkText') { - return [arg.type === 'Literal' + return [isStringLiteral(arg) ? j.literal(`*=${arg.value}`) : j.templateLiteral([ j.templateElement({ raw: '*=', cooked: '*=' }, false) ], [arg]) ] } else if (bySelector === 'name') { - return [arg.type === 'Literal' + return [isStringLiteral(arg) ? j.literal(`*[name="${arg.value}"]`) : j.templateLiteral([ j.templateElement({ raw: '*[name="', cooked: '*[name="' }, false), @@ -118,14 +123,14 @@ function getSelectorArgument (j, path, callExpr, file) { ], [arg]) ] } else if (bySelector === 'className') { - return [arg.type === 'Literal' + return [isStringLiteral(arg) ? j.literal(`.${arg.value}`) : j.templateLiteral([ j.templateElement({ raw: '.', cooked: '.' }, false) ], [arg]) ] } else if (bySelector === 'options') { - return [arg.type === 'Literal' + return [isStringLiteral(arg) ? j.literal(`select[ng-options="${arg.value}"] option`) : j.templateLiteral([ j.templateElement({ raw: 'select[ng-options="', cooked: 'select[ng-options="' }, false), @@ -133,14 +138,14 @@ function getSelectorArgument (j, path, callExpr, file) { ], [arg]) ] } else if (bySelector === 'buttonText') { - return [arg.type === 'Literal' + return [isStringLiteral(arg) ? j.literal(`button=${arg.value}`) : j.templateLiteral([ j.templateElement({ raw: 'button=', cooked: 'button=' }, false) ], [arg]) ] } else if (bySelector === 'partialButtonText') { - return [arg.type === 'Literal' + return [isStringLiteral(arg) ? j.literal(`button*=${arg.value}`) : j.templateLiteral([ j.templateElement({ raw: 'button*=', cooked: 'button*=' }, false) @@ -439,28 +444,53 @@ const filterElementCalls = ({ value: { argument: { callee: { property: { name } ELEMENT_COMMANDS.includes(name) || ELEM_PROPS.includes(name) ) -const filterFor = (type) => ({ +const filterFor = { argument: { callee: { object: { - type: 'MemberExpression', - property: { type } + type: 'MemberExpression' } } } -}) +} function sanitizeAsyncCalls (j, root) { - root.find(j.AwaitExpression, filterFor('Identifier')) + root.find(j.AwaitExpression, filterFor) + .filter(({ value: { argument: { callee: { object } } } }) => ( + ( + object.object.type === 'MemberExpression' || + object.property.type !== 'Literal' + ) + && object.object.type !== 'AwaitExpression' + )) .filter(filterElementCalls) .replaceWith(({ value: { argument } }) => ( j.awaitExpression( j.callExpression( j.memberExpression( - j.awaitExpression(argument.callee.object), + isNumericalLiteral(argument.callee.object.property) + ? j.memberExpression( + j.awaitExpression(argument.callee.object.object), + argument.callee.object.property, + true + ) + : j.awaitExpression( + j.memberExpression( + argument.callee.object.object, + argument.callee.object.property, + isNumericalLiteral(argument.callee.object.property) + ) + ), argument.callee.property ), - argument.arguments + argument.arguments, + true ) ) )) - root.find(j.AwaitExpression, filterFor('Literal')) + root.find(j.AwaitExpression, filterFor) + .filter(({ value: { argument } }) => { + if (argument.callee.object) { + return argument.callee.object.property.type === 'NumericLiteral' + } + return true + }) .filter(filterElementCalls) .filter(({ value: { argument: { callee } } }) => callee.object.object.type === 'MemberExpression') .replaceWith(({ value: { argument } }) => ( @@ -498,6 +528,17 @@ function makeAsync ({ value, parentPath }) { return value } +function failAsyncConstructor (path, file) { + throw new TransformError('' + + `With "this.${path.value.property.name}" you are ` + + 'trying to access an element within a constructor. Given that it ' + + 'is not possible to run asynchronous code in this context, it ' + + 'is advised to move this call into a method or getter function.', + path.value, + file + ) +} + module.exports = { isCustomStrategy, TransformError, @@ -506,5 +547,6 @@ module.exports = { replaceCommands, parseConfigProperties, sanitizeAsyncCalls, - makeAsync + makeAsync, + failAsyncConstructor } diff --git a/test/__fixtures__/protractor/transformed/conf.js b/test/__fixtures__/protractor/transformed/conf.js index 8953db3..4e45159 100644 --- a/test/__fixtures__/protractor/transformed/conf.js +++ b/test/__fixtures__/protractor/transformed/conf.js @@ -41,19 +41,6 @@ exports.config = { framework: "jasmine", framework: 'jasmine', - - autoCompileOpts: { - autoCompile: true, - - tsNodeOpts: { - project: require('path').join(__dirname, './tsconfig.e2e.json') - }, - - babelOpts: { - presets: [ 'es2015' ] - } - }, - user: process.env.SAUCE_USERNAME, key: process.env.SAUCE_ACCESS_KEY, region: 'eu-central-1', @@ -100,5 +87,17 @@ exports.config = { protocol: "https", port: 443, - hostname: "api.kobiton.com" + hostname: "api.kobiton.com", + + autoCompileOpts: { + autoCompile: true, + + tsNodeOpts: { + project: require('path').join(__dirname, './tsconfig.e2e.json') + }, + + babelOpts: { + presets: [ 'es2015' ] + } + } }; diff --git a/test/__fixtures__/v7/transformed/compilerFunctions.js b/test/__fixtures__/v7/transformed/compilerFunctions.js index 257a32f..1a9f59d 100644 --- a/test/__fixtures__/v7/transformed/compilerFunctions.js +++ b/test/__fixtures__/v7/transformed/compilerFunctions.js @@ -1,6 +1,21 @@ exports.config = { framework: 'jasmine', + cucumberOpts: { + requireModule: [() => { + console.log('foo'); + console.log('bar'); + }] + }, + + jasmineOpts: { + requires: [] + }, + + mochaOpts: { + require: [async function () {}] + }, + autoCompileOpts: { autoCompile: true, @@ -16,20 +31,5 @@ exports.config = { babelOpts: { ignore: [] } - }, - - cucumberOpts: { - requireModule: [() => { - console.log('foo'); - console.log('bar'); - }] - }, - - jasmineOpts: { - requires: [] - }, - - mochaOpts: { - require: [async function () {}] } } diff --git a/test/__fixtures__/v7/transformed/spec.js b/test/__fixtures__/v7/transformed/spec.js index 872c5a4..344304b 100644 --- a/test/__fixtures__/v7/transformed/spec.js +++ b/test/__fixtures__/v7/transformed/spec.js @@ -4,6 +4,20 @@ const { Given2, When2, Then2 } = require("@cucumber/cucumber"); exports.config = { framework: 'jasmine', + mochaOpts: { + ui: 'bdd', + timeout: 5000, + require: ['/foo/bar'] + }, + + jasmineOpts: { + requires: [] + }, + + cucumberOpts: { + requireModule: [] + }, + autoCompileOpts: { autoCompile: true, @@ -19,19 +33,5 @@ exports.config = { tsNodeOpts: { bar: 'foo' } - }, - - mochaOpts: { - ui: 'bdd', - timeout: 5000, - require: ['/foo/bar'] - }, - - jasmineOpts: { - requires: [] - }, - - cucumberOpts: { - requireModule: [] } } diff --git a/test/runner.js b/test/runner.js index 244159c..78f38a7 100644 --- a/test/runner.js +++ b/test/runner.js @@ -5,6 +5,7 @@ const expect = require('expect') const Runner = require('jscodeshift/src/Runner') +const supportedParsers = ['babel', 'tsx'] const frameworkTests = { protractor: [ ['./conf.js', './conf.js'], @@ -39,7 +40,7 @@ const frameworkTests = { let error -async function runTest (framework, tests) { +async function runTest (framework, tests, parser = 'babel') { shell.cp( '-r', path.join(__dirname, '__fixtures__', framework, 'source'), @@ -52,7 +53,8 @@ async function runTest (framework, tests) { path.resolve(path.join(__dirname, '..', framework, 'index.js')), [srcFile], { - verbose: 2 + verbose: 2, + parser } ) @@ -78,14 +80,19 @@ async function runTest (framework, tests) { ;(async () => { const teardown = () => shell.rm('-r', path.join(__dirname, 'testdata')) - const testsToRun = process.argv.length === 3 + const testsToRun = process.argv.length === 3 && Object.keys(frameworkTests).includes(process.argv[2]) ? { [process.argv[2]]: frameworkTests[process.argv[2]] } : frameworkTests + const parserToRun = process.argv.length === 3 && supportedParsers.includes(process.argv[2]) + ? [process.argv[2]] + : supportedParsers for (const [framework, tests] of Object.entries(testsToRun)) { - console.log('========================') - console.log(`Run tests for ${framework}`) - console.log('========================\n') - await runTest(framework, tests).finally(teardown) + for (const parser of parserToRun) { + console.log('================================================') + console.log(`Run tests for ${framework} using ${parser} parser`) + console.log('================================================\n') + await runTest(framework, tests, parser).finally(teardown) + } } })().then( () => console.log('Tests passed ✅'), diff --git a/v6/index.js b/v6/index.js index 6919e4d..c8eaf8c 100644 --- a/v6/index.js +++ b/v6/index.js @@ -2,7 +2,7 @@ const { paramCase } = require('param-case') const { COMMAND_TRANSFORMS, COMMANDS_WITHOUT_FIRST_PARAM, SERVICE_PROPS, SERVICE_PROP_MAPPING } = require('./constants') -module.exports = function transformer(file, api) { +module.exports = function transformer(file, api, opts) { const j = api.jscodeshift; const root = j(file.source); @@ -98,7 +98,7 @@ module.exports = function transformer(file, api) { }) } - root.find(j.Property, { + root.find(opts.parser === 'babel' ? j.Property : j.ObjectProperty, { key: { name: 'services' } }).replaceWith((path) => j.property( 'init', diff --git a/v7/index.js b/v7/index.js index 45d41e4..059012a 100644 --- a/v7/index.js +++ b/v7/index.js @@ -1,9 +1,9 @@ const compilers = require('../common/compilers') -module.exports = function transformer(file, api) { +module.exports = function transformer(file, api, opts) { const j = api.jscodeshift; const root = j(file.source); - const autoCompileOpts = compilers.remove(j, root) + const autoCompileOpts = compilers.remove(j, root, opts) /** * transforms imports from `require('cucumber')` to `require('@cucumber/cucumber')` @@ -28,6 +28,6 @@ module.exports = function transformer(file, api) { ) )) - compilers.update(j, root, autoCompileOpts) + compilers.update(j, root, autoCompileOpts, opts) return root.toSource() }