UNPKG

@wdio/codemod

Version:

A codemod to transform test files written in Protractor into WebdriverIO tests

878 lines (828 loc) 30.3 kB
const { format } = require('util') const camelCase = require('camelcase') const flattenDeep = require('lodash.flattendeep') const compilers = require('../common/compilers') const { SUPPORTED_SELECTORS, PRTRCTR_ELEMENT_COMMANDS, SELECTOR_COMMANDS, UNSUPPORTED_COMMANDS, COMMANDS_TO_REMOVE, UNSUPPORTED_COMMAND_ADVICE, UNSUPPORTED_COMMAND_ERROR, INCOMPATIBLE_COMMAND_ERROR, REPLACE_WINDOW_COMMANDS, REPLACE_TIMEOUTS, REPLACE_MANAGE_COMMANDS, REPLACE_NAVIGATE_COMMANDS } = require('./constants') const { ELEMENT_COMMANDS } = require('../common/constants') const { isCustomStrategy, TransformError, getSelectorArgument, matchesSelectorExpression, replaceCommands, parseConfigProperties, sanitizeAsyncCalls, failAsyncConstructor, makeAsync } = require('./utils') module.exports = function transformer(file, api, opts) { const j = api.jscodeshift; const root = j(file.source); j.file = file /** * transform Protractors config file * ```js * exports.config = { * // ... * } * ``` */ root.find(j.ExpressionStatement, { expression: { type: 'AssignmentExpression', left: { object: { name: 'exports' }, property: { name: 'config' } }, right: { type: 'ObjectExpression' } } }).replaceWith((path) => { const props = path.value.expression.right.properties let hasKobitonUserProp = props.find(({ key }) => (key.name || key.value) === 'kobitonUser') let hasKobitonKeyProp = props.find(({ key }) => (key.name || key.value) === 'kobitonKey') const kobitonConnectionOptions = hasKobitonUserProp && hasKobitonKeyProp ? [ j.objectProperty( j.identifier('protocol'), j.literal('https') ), j.objectProperty( j.identifier('port'), j.identifier('443') ), j.objectProperty( j.identifier('hostname'), j.literal('api.kobiton.com') ) ] : [] return ( j.expressionStatement( j.assignmentExpression( '=', path.value.expression.left, j.objectExpression( flattenDeep( props .map(parseConfigProperties.bind(j)) .filter(Boolean) .concat(kobitonConnectionOptions) ) ) ) ) ) }) const autoCompileOpts = compilers.remove(j, root, opts) /** * remove all protractor import declarations */ root.find(j.ImportDeclaration, { source: { value: 'protractor' } }).remove() root.find(j.VariableDeclaration, { declarations: [{ init: { arguments: [{ value: 'protractor' }] } }] }).remove() /** * remove `require("babel-register")({ ... })` */ root.find(j.ExpressionStatement, { expression: { callee: { callee: { name: 'require' }, arguments: [{ value: 'babel-register' }] } } }).remove() /** * remove all `jasmine.getEnv()` */ root.find(j.ExpressionStatement, { expression: { callee: { object: { callee: { name: 'require' }, arguments: [{ value: 'ts-node' }] }, property: { name: 'register' } } } }).remove() /** * remove all `require('ts-node').register({ ... })` */ root.find(j.ExpressionStatement, { expression: { callee: { object: { callee: { object: { name: 'jasmine' }, property: { name: 'getEnv' } } } } } }).remove() /** * remove command statements that aren't useful in WebdriverIO world, e.g. * await $('body').allowAnimations(false); * browser.waitForAngularEnabled(true) */ root.find(j.ExpressionStatement) .filter(({ value: { expression } }) => { const expr = ( expression.callee || ( expression.argument && expression.argument.callee ) ) return (expr && expr.property && COMMANDS_TO_REMOVE.includes(expr.property.name)) }) .remove() /** * remove unsupported assignments like * browser.ignoreSynchronization = true; */ root.find(j.ExpressionStatement, { expression: { left: { object: { name: 'browser' }, property: { name: 'ignoreSynchronization' } } } }).remove() /** * transform_ * browser.driver.findElement * browser.findElement */ root.find(j.MemberExpression, { object: { name: 'browser' }, property: { name: 'driver' } }).replaceWith((path) => j.identifier('browser')) /** * transform: * element(...) * $('...') */ root.find(j.CallExpression, { callee: { name: 'element' } }) .filter((path) => matchesSelectorExpression(path)) .replaceWith((path) => { const isCustomStrategy = !SUPPORTED_SELECTORS.includes(path.value.arguments[0].callee.property.name) if (isCustomStrategy) { return j.callExpression( j.memberExpression( j.identifier('browser'), j.identifier('custom$') ), getSelectorArgument(j, path, path.value.arguments[0], file) ) } return j.callExpression( j.identifier('$'), getSelectorArgument(j, path, path.value.arguments[0], file) ) }) /** * transform: * element.all(...) * $$('...') */ root.find(j.CallExpression, { callee: { type: 'MemberExpression', property: { name: 'all' } } }) .filter((path) => matchesSelectorExpression(path)) .replaceWith((path) => { const scope = path.value.callee.object const isCustomStrategy = !SUPPORTED_SELECTORS.includes(path.value.arguments[0].callee.property.name) const hasCustomScope = scope.name !== 'element' if (isCustomStrategy) { return j.callExpression( hasCustomScope ? scope : j.memberExpression( j.identifier('browser'), j.identifier('custom$$') ), getSelectorArgument(j, path, path.value.arguments[0], file) ) } return j.callExpression( hasCustomScope ? j.memberExpression( scope, j.identifier('$$') ) : j.identifier('$$'), getSelectorArgument(j, path, path.value.arguments[0], file) ) }) /** * transform: * $$('...').get(0) * $$('...')[0] */ root.find(j.CallExpression, { callee: { object: { callee: { name: '$$' } }, property: { name: 'get' } } }) .replaceWith((path) => j.memberExpression( path.value.callee.object, path.value.arguments[0], true )) /** * transform * browser.actions().sendKeys(protractor.Key.ENTER).perform(); * browser.keys('Enter') */ root.find(j.CallExpression, { callee: { object: { callee: { object: { callee: { property: { name: 'actions' } } }, property: { name: 'sendKeys' } } } } }).replaceWith((path) => { const param = path.value.callee.object.arguments[0] if (!param.object.object || param.object.object.name !== 'protractor' || param.object.property.name !== 'Key') { throw new TransformError('' + 'Expected "proctractor.Key.XXX" as argument to the sendKeys command. ' + 'Please raise an issue in the codemod repository: https://github.com/webdriverio/codemod/issues/new', path.value, file ) } const key = param.property.name.slice(0, 1).toUpperCase() + camelCase(param.property.name).slice(1) return j.callExpression( j.memberExpression( j.identifier('browser'), j.identifier('keys') ), [ j.literal(key) ] ) }) /** * transform browser commands */ root.find(j.CallExpression, { callee: { type: 'MemberExpression', object: { name: 'browser' } } }).replaceWith((path) => { const method = path.value.callee.property.name let args = path.value.arguments /** * throw error for methods that can't be transformed */ if (method === 'touchActions') { throw new TransformError( format(INCOMPATIBLE_COMMAND_ERROR, method, 'https://webdriver.io/docs/api/browser/touchAction'), path.value, file ) } else if (method === 'actions') { throw new TransformError( format(INCOMPATIBLE_COMMAND_ERROR, method, 'https://webdriver.io/docs/api/webdriver#performactions'), path.value, file ) } else if (method === 'setLocation') { throw new TransformError( format(INCOMPATIBLE_COMMAND_ERROR, method, 'https://webdriver.io/docs/api/browser/url'), path.value, file ) } else if (UNSUPPORTED_COMMANDS.includes(method)) { throw new TransformError( format(UNSUPPORTED_COMMAND_ERROR, method, 'https://github.com/webdriverio/codemod/issues/new'), path.value.callee.property, file ) } else if (method === 'getProcessedConfig') { return j.memberExpression( path.value.callee.object, j.identifier('config') ) } else if (['findElement', 'findElements'].includes(method)) { return j.callExpression( j.memberExpression( path.value.callee.object, j.identifier(method === 'findElement' ? '$' : '$$') ), getSelectorArgument(j, path, args[0], file) ) } else if (method === 'get') { args = path.value.arguments.slice(0, 1) } else if (method === 'wait' && args.length > 1) { return j.callExpression( j.memberExpression( path.value.callee.object, j.identifier(replaceCommands(method)) ), [ args[0], j.objectExpression([ ...(args[1] ? [ j.objectProperty( j.identifier('timeout'), args[1] ) ] : [] ), ...(args[2] ? [ j.objectProperty( j.identifier('timeoutMsg'), args[2] ) ] : [] ) ]) ] ) } return j.callExpression( j.memberExpression( path.value.callee.object, j.identifier(replaceCommands(method)) ), args ) }) /** * transform * - `browser.manage().logs().get(...)` * - `browser.driver.manage().timeouts().implicitlyWait(15000);` * - `browser.driver.manage().window().maximize();` */ root.find(j.CallExpression) .filter((path) => ( path.value.callee.property && [ 'get', ...Object.keys(REPLACE_TIMEOUTS), ...Object.keys(REPLACE_WINDOW_COMMANDS), ...Object.keys(REPLACE_MANAGE_COMMANDS), ...Object.keys(REPLACE_NAVIGATE_COMMANDS) ].includes(path.value.callee.property.name) && path.value.callee.object && path.value.callee.object.callee && path.value.callee.object.callee.property && ['logs', 'timeouts', 'window', 'manage', 'navigate'].includes(path.value.callee.object.callee.property.name) )) .replaceWith((path) => { const scope = path.value.callee.object.callee.property.name const command = path.value.callee.property.name if (scope === 'logs') { let logType = path.value.arguments[0].property.name.toLowerCase() return j.callExpression( j.memberExpression( j.identifier('browser'), j.identifier('getLogs') ), [ j.literal(logType) ] ) } else if (scope === 'timeouts') { const timeout = path.value.arguments[0].value return j.callExpression( j.memberExpression( j.identifier('browser'), j.identifier('setTimeout') ), [ j.objectExpression([ j.objectProperty( j.identifier(REPLACE_TIMEOUTS[command]), j.literal(timeout) ) ]) ] ) } else if (scope === 'window') { const args = [] if (command === 'setSize') { args.push( j.literal(0), j.literal(0), path.value.arguments[0], path.value.arguments[1] ) } return j.callExpression( j.memberExpression( j.identifier('browser'), j.identifier(REPLACE_WINDOW_COMMANDS[command]) ), args ) } else if (scope === 'manage') { const args = [] if (command === 'addCookie') { args.push(path.value.arguments[0]) } else if (command === 'deleteCookie' || command === 'getCookie') { const cookieValue = path.value.arguments[0].value; args.push(j.literal(cookieValue)); } return j.callExpression( j.memberExpression( j.identifier('browser'), j.identifier(REPLACE_MANAGE_COMMANDS[command]) ), args ) } else if (scope === 'navigate') { const args = []; if (command === 'to') { const urlValue = path.value.arguments[0].value; args.push(j.literal(urlValue)); } return j.callExpression( j.memberExpression( j.identifier('browser'), j.identifier(REPLACE_NAVIGATE_COMMANDS[command]) ), args ) } }) /** * transform element commands */ root.find(j.CallExpression) .filter((path) => ( path.value.callee && path.value.callee.type === 'MemberExpression' && PRTRCTR_ELEMENT_COMMANDS.includes(path.value.callee.property.name) )) .replaceWith((path) => { const command = path.value.callee.property.name if (command === 'getCssValue') { throw new TransformError( format(INCOMPATIBLE_COMMAND_ERROR, command, 'https://webdriver.io/docs/api/element/getCSSProperty/'), path.value.callee.property, file ) } else if (command === 'submit') { throw new TransformError( format( UNSUPPORTED_COMMAND_ADVICE, command, 'use the click command to click on the submit button', 'https://webdriver.io/docs/api/element/click' ), path.value, file ) } else if (UNSUPPORTED_COMMANDS.includes(command)) { throw new TransformError(format( UNSUPPORTED_COMMAND_ERROR, command, 'https://github.com/webdriverio/codemod/issues/new' ), path.value.callee.property, file) } else if (command === 'getWebElement') { return path.value.callee.object } else if (command === 'getId') { return j.memberExpression( path.value.callee.object, j.identifier('elementId') ) } else if (command === 'isElementPresent') { /** * transform `element(by.css('#abc')).isElementPresent(by.css('#def'))` * to `$('#abc').$('#def')` */ return j.callExpression( j.memberExpression( j.callExpression( j.memberExpression( path.value.callee.object, j.identifier('$') ), getSelectorArgument(j, path, path.value.arguments[0], file) ), j.identifier('isExisting') ), [] ) } else if (command === 'count') { return j.memberExpression( path.value.callee.object, j.identifier('length') ) } else if ( /** * check if element was called with get, e.g. * this.deleteButtons.get(0).click(); */ path.value.callee.object.callee && path.value.callee.object.callee.property && path.value.callee.object.callee.property.name === 'get' ) { return j.callExpression( j.memberExpression( j.memberExpression( path.value.callee.object.callee.object, path.value.callee.object.arguments[0], true ), j.identifier(replaceCommands(command)) ), path.value.arguments ) } /** * transform any other element command */ let object = path.value.callee.object if ( path.value.callee.object.type === 'AwaitExpression' && path.parentPath.value.type === 'AwaitExpression' ) { object = object.argument } return j.callExpression( j.memberExpression( object, j.identifier(replaceCommands(command)) ), path.value.arguments ) }) /** * transform element chaining */ root.find(j.CallExpression) .filter((path) => ( path.value.callee && path.value.callee.type === 'MemberExpression' && ['element', 'elements', 'findElement', 'findElements'].includes(path.value.callee.property.name) )) .replaceWith((path) => { const isCustomStrategy = !SUPPORTED_SELECTORS.includes(path.value.arguments[0].callee.property.name) const singleElementCall = ['element', 'findElement'] const chainedCommand = singleElementCall.includes(path.value.callee.property.name) ? isCustomStrategy ? 'custom$' : '$' : isCustomStrategy ? 'custom$$' : '$$' return j.callExpression( j.memberExpression( path.value.callee.object, j.identifier(chainedCommand) ), getSelectorArgument(j, path, path.value.arguments[0], file) ) }) /** * replace await/then calls, e.g. * ``` * await browser.getAllWindowHandles().then(handles => { * browser.switchTo().window(handles[handles.length - 1]); * }) * ``` * to: * ``` * const handles = await browser.getAllWindowHandles() * browser.switchTo().window(handles[handles.length - 1]); * ``` */ root.find(j.ExpressionStatement, { expression: { type: 'AwaitExpression', argument: { type: 'CallExpression', callee: { property: { name: 'then' } } } } }).replaceWith((path) => [ j.variableDeclaration( 'let', [ j.variableDeclarator( j.identifier(path.value.expression.argument.arguments[0].params[0].name), j.awaitExpression(path.value.expression.argument.callee.object) ) ] ), ...path.value.expression.argument.arguments[0].body.body ]) /** * transform by.addLocator */ root.find(j.CallExpression, { callee: { type: 'MemberExpression', object: { name: 'by' }, property: { name: 'addLocator' } } }).replaceWith((path) => { /** * check if user uses rootSelector parameter which is not supported * in WebdriverIO */ if (path.value.arguments[1].params.length > 2) { const errorText = '' + `WebdriverIO doesn't support the "rootSelector" ` + `parameter in the custom locator function.` throw new TransformError(errorText, path, file) } return j.callExpression( j.memberExpression( j.identifier('browser'), j.identifier('addLocatorStrategy') ), path.value.arguments ) }) /** * transform `browser.switchTo().frame('composeWidget');` */ root.find(j.MemberExpression) .filter((path) => ( ['frame', 'window'].includes(path.value.property.name) && path.value.object.callee && path.value.object.callee.property.name === 'switchTo' )) .replaceWith((path) => j.memberExpression( j.identifier('browser'), j.identifier(replaceCommands(path.value.property.name)) )) /** * transform * - const EC = protractor.ExpectedConditions; * into * - const EC = require('wdio-wait-for') */ const wdioWaitForImport = j.callExpression( j.identifier('require'), [ j.literal('wdio-wait-for') ] ) root.find(j.VariableDeclaration, { declarations: [{ init: { object: { name: 'protractor' }, property: { name: 'ExpectedConditions' } } }] }).replaceWith((path) => j.variableDeclaration( path.value.kind, [ j.variableDeclarator( path.value.declarations[0].id, wdioWaitForImport ) ] )) root.find(j.ExpressionStatement, { expression: { right: { object: { name: 'protractor' }, property: { name: 'ExpectedConditions' } } } }).replaceWith((path) => j.expressionStatement( j.assignmentExpression( '=', path.value.expression.left, wdioWaitForImport ) )) /** * transform expected conditions not put into a var */ root.find(j.MemberExpression, { object: { object: { name: 'protractor' }, property: { name: 'ExpectedConditions' } } }).replaceWith((path) => j.memberExpression( wdioWaitForImport, path.value.property )) /** * no support */ root.find(j.MemberExpression, { object: { name: 'protractor' } }).replaceWith((path) => { throw new TransformError('' + `"${path.value.object.name}.${path.value.property.name}" ` + 'is unknown to this codemod. If this kind of code appears ' + 'often in your code base, please raise an issue in the codemod ' + 'repository: https://github.com/webdriverio/codemod/issues/new.', path.value, file ) }) /** * transform element declarations in class constructors into getters */ const elementGetters = new Map() const setGetters = (path) => { const isElementDeclaration = (e) => ( e.expression && e.expression.type === 'AssignmentExpression' && e.expression.left.object && e.expression.left.object.type === 'ThisExpression' && e.expression.left.property && e.expression.left.property.type === 'Identifier' && ( e.expression.right.callee && SELECTOR_COMMANDS.includes(e.expression.right.callee.name) || e.expression.right.arguments && e.expression.right.arguments.find((arg) => arg.callee && SELECTOR_COMMANDS.includes(arg.callee.name)) ) ) 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.classMethod(kind, key, params, j.blockStatement(body.body.filter((e) => !isElementDeclaration(e)))), ...[...elementGetters.entries()].map(([elemName, object]) => j.methodDefinition( 'get', elemName, j.functionExpression( null, [], j.blockStatement([ j.returnStatement(object) ]) ) )) ] } 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. * await friendPage.addnameBox.setValue('Some text...'); * to: * await (await friendPage.addnameBox).setValue('Some text...'); */ const ELEM_PROPS = ['length'] const filterElementCalls = ({ value: { expression: { argument } } }) => ( argument && argument.callee && argument.callee.property && ( ELEMENT_COMMANDS.includes(argument.callee.property.name) || ELEM_PROPS.includes(argument.callee.property.name) ) ) root.find(j.ExpressionStatement) .filter(filterElementCalls) .forEach((path) => sanitizeAsyncCalls(j, j(path))) /** * transform lazy loaded elements calls in async context, e.g. * await friendPage.addnameBox[0].setValue('Some text...'); * to: * await (await friendPage.addnameBox)[0].setValue('Some text...'); */ root.find(j.ExpressionStatement) .filter(filterElementCalls) .forEach((path) => sanitizeAsyncCalls(j, j(path))) /** * transform lazy loaded elements calls in async context, e.g. * expect(await searchPage.noResultsMsg.isDisplayed()).toBe(true); * to: * expect(await (await searchPage.noResultsMsg).isDisplayed()).toBe(true); */ root.find(j.ExpressionStatement, { expression: { callee: { object: { callee: { name: 'expect' } } } } }).forEach((path) => sanitizeAsyncCalls(j, j(path))) /** * find all `this` with properties we know are elements and make them async */ root.find(j.MemberExpression, { object: { type: 'ThisExpression' } }).filter((path) => ( path.parentPath.value.type !== 'AwaitExpression' && [...elementGetters.keys()].map((p) => p.name).includes(path.value.property.name) )).replaceWith((path) => { j(path).closest(j.FunctionExpression).replaceWith(makeAsync) j(path).closest(j.ArrowFunctionExpression).replaceWith(makeAsync) 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, opts) return root.toSource(opts.printOptions) }