@wdio/codemod
Version:
A codemod to transform test files written in Protractor into WebdriverIO tests
553 lines (526 loc) • 19.6 kB
JavaScript
const url = require('url')
const { format } = require('util')
const {
isStringLiteral,
isNumericalLiteral
} = require('../common/utils')
const {
IGNORED_CONFIG_PROPERTIES,
UNSUPPORTED_CONFIG_OPTION_ERROR,
UNSUPPORTED_SELECTOR_STRATEGIES,
REPLACE_CONFIG_KEYS,
IGNORED_CAPABILITIES
} = require('./constants')
const { ELEMENT_COMMANDS } = require('../common/constants')
function isCustomStrategy (path) {
return !SUPPORTED_SELECTORS.includes(
path.value.arguments[0].callee.property.name
)
}
class TransformError extends Error {
constructor(message, expr, file) {
const source = file.source.split('\n')
const line = source.slice(expr.loc.start.line - 1, expr.loc.end.line)[0]
const expression = line.slice(0, expr.loc.end.column)
const errorMsg = `Error transforming ${file.path.replace(process.cwd(), '')}:${expr.loc.start.line}`
super(errorMsg)
this.stack = (
errorMsg + '\n\n' +
`> ${expression}\n` +
' '.repeat(expr.loc.start.column + 2) + '^\n\n' +
message + '\n' +
` at ${file.path}:${expr.loc.start.line}:${expr.loc.start.column}`
)
this.name = this.constructor.name
}
}
function getSelectorArgument (j, path, callExpr, file) {
const bySelector = callExpr.callee.property.name
const arg = callExpr.arguments[0]
if (Object.keys(UNSUPPORTED_SELECTOR_STRATEGIES).includes(bySelector)) {
throw new TransformError(
UNSUPPORTED_SELECTOR_STRATEGIES[bySelector],
path.value,
file
)
} else if (bySelector === 'id') {
return [isStringLiteral(arg)
? j.literal(`#${arg.value}`)
: j.templateLiteral([
j.templateElement({ raw: '#', cooked: '#' }, false)
], [
arg
])
]
} else if (bySelector === 'model') {
return [isStringLiteral(arg)
? j.literal(`*[ng-model="${arg.value}"]`)
: j.templateLiteral([
j.templateElement({ raw: '*[ng-model="', cooked: '*[ng-model="' }, false),
j.templateElement({ raw: '"]', cooked: '"]' }, true),
], [
arg
])
]
} else if (bySelector === 'repeater') {
return [isStringLiteral(arg)
? j.literal(`*[ng-repeat="${arg.value}"]`)
: j.templateLiteral([
j.templateElement({ raw: '*[ng-repeat="', cooked: '*[ng-repeat="' }, false),
j.templateElement({ raw: '"]', cooked: '"]' }, true),
], [
arg
])
]
} else if (bySelector === 'css') {
return [...callExpr.arguments]
} else if (bySelector === 'cssContainingText') {
const text = callExpr.arguments[1]
if (text.regex) {
throw new TransformError('this codemod does not support RegExp in cssContainingText', path.value, file)
} else if (isStringLiteral(text)) {
return [j.literal(`${arg.value}=${text.value}`)]
} else if (text.type === 'Identifier') {
return [
j.binaryExpression(
'+',
j.literal(arg.value + '='),
j.identifier(text.name)
)
]
} else {
throw new TransformError('expect 2nd parameter of cssContainingText to be a literal or identifier', path.value, file)
}
} else if (bySelector === 'xpath' || bySelector === 'tagName' || bySelector === 'js') {
return [arg]
} else if (bySelector === 'linkText') {
return [isStringLiteral(arg)
? j.literal(`=${arg.value}`)
: j.templateLiteral([
j.templateElement({ raw: '=', cooked: '=' }, false)
], [arg])
]
} else if (bySelector === 'partialLinkText') {
return [isStringLiteral(arg)
? j.literal(`*=${arg.value}`)
: j.templateLiteral([
j.templateElement({ raw: '*=', cooked: '*=' }, false)
], [arg])
]
} else if (bySelector === 'name') {
return [isStringLiteral(arg)
? j.literal(`*[name="${arg.value}"]`)
: j.templateLiteral([
j.templateElement({ raw: '*[name="', cooked: '*[name="' }, false),
j.templateElement({ raw: '"]', cooked: '"]' }, true)
], [arg])
]
} else if (bySelector === 'className') {
return [isStringLiteral(arg)
? j.literal(`.${arg.value}`)
: j.templateLiteral([
j.templateElement({ raw: '.', cooked: '.' }, false)
], [arg])
]
} else if (bySelector === 'options') {
return [isStringLiteral(arg)
? j.literal(`select[ng-options="${arg.value}"] option`)
: j.templateLiteral([
j.templateElement({ raw: 'select[ng-options="', cooked: 'select[ng-options="' }, false),
j.templateElement({ raw: '"] option', cooked: '"] option' }, true)
], [arg])
]
} else if (bySelector === 'buttonText') {
return [isStringLiteral(arg)
? j.literal(`button=${arg.value}`)
: j.templateLiteral([
j.templateElement({ raw: 'button=', cooked: 'button=' }, false)
], [arg])
]
} else if (bySelector === 'partialButtonText') {
return [isStringLiteral(arg)
? j.literal(`button*=${arg.value}`)
: j.templateLiteral([
j.templateElement({ raw: 'button*=', cooked: 'button*=' }, false)
], [arg])
]
}
// we assume a custom locator strategy
const selectorStrategyName = callExpr.callee.property.name
const selector = callExpr.arguments[0].value
return [
j.literal(selectorStrategyName),
j.literal(selector)
]
}
function matchesSelectorExpression (path) {
return (
path.value.arguments.length === 1 &&
path.value.arguments[0].callee.type === 'MemberExpression' &&
path.value.arguments[0].callee.object.name === 'by'
)
}
function replaceCommands (prtrctrCommand) {
switch (prtrctrCommand) {
// element commands
case 'sendKeys':
return 'setValue'
case 'isPresent':
return 'isExisting'
case 'getDriver':
return 'parentElement'
// browser commands
case 'executeScript':
return 'execute'
case 'getPageSource':
return 'getSource'
case 'get':
return 'url'
case 'sleep':
return 'pause'
case 'frame':
return 'switchToFrame'
case 'window':
return 'switchToWindow'
case 'enterRepl':
case 'explore':
return 'debug'
case 'getCurrentUrl':
case 'getLocationAbsUrl':
return 'getUrl'
case 'wait':
return 'waitUntil'
case 'close':
return 'closeWindow'
case 'getPosition':
return 'getWindowRect'
case 'setSize':
return 'setWindowRect'
case 'restart':
case 'restartSync':
return 'reloadSession'
case 'getAllWindowHandles':
return 'getWindowHandles'
case 'clear':
return 'clearValue'
default: return prtrctrCommand
}
}
function parseSeleniumAddress (value) {
const u = url.parse(value)
remoteHostname = u.hostname
return [
this.objectProperty(
this.identifier('protocol'),
this.stringLiteral(u.protocol.slice(0, -1))
),
this.objectProperty(
this.identifier('hostname'),
this.stringLiteral(u.hostname)
),
this.objectProperty(
this.identifier('port'),
this.literal(parseInt(u.port || '443'))
),
this.objectProperty(
this.identifier('path'),
this.stringLiteral(u.path)
)
]
}
let remoteHostname = null
function parseConfigProperties (property) {
const name = property.key.name || property.key.value
const value = property.value.value
if (name === 'seleniumAddress') {
return parseSeleniumAddress.call(this, value)
} else if (name === 'capabilities' && property.value.properties) {
const { rootLevelConfigs, parsedCaps } = parseCapabilities.call(this, property.value.properties)
return [
...rootLevelConfigs,
this.objectProperty(
this.identifier(name),
this.arrayExpression([this.objectExpression(parsedCaps)])
)
]
} else if (name === 'multiCapabilities') {
const rlc = []
const pc = []
for (const caps of property.value.elements) {
if (!caps.properties) {
continue
}
const { rootLevelConfigs, parsedCaps } = parseCapabilities.call(this, caps.properties)
rlc.push(...rootLevelConfigs)
pc.push(this.objectExpression(parsedCaps))
}
return [
...rlc,
this.objectProperty(
this.identifier('capabilities'),
this.arrayExpression(pc)
)
]
} else if (REPLACE_CONFIG_KEYS[name]) {
return this.objectProperty(
this.identifier(REPLACE_CONFIG_KEYS[name]),
property.value
)
} else if (name === 'framework' && value === 'jasmine2') {
return this.objectProperty(
this.identifier('framework'),
this.literal('jasmine')
)
} else if (name === 'suites') {
return this.objectProperty(
this.identifier('suites'),
this.objectExpression(property.value.properties.map((prop) => (
this.objectProperty(
this.identifier(prop.key.name),
this.arrayExpression([this.literal(prop.value.value)])
)
)))
)
} else if (name === 'seleniumServerJar') {
throw new TransformError(format(
UNSUPPORTED_CONFIG_OPTION_ERROR,
name,
'the "@wdio/selenium-standalone-service"',
'https://webdriver.io/docs/selenium-standalone-service'
), property.value, this.file)
} else if (name === 'localSeleniumStandaloneOpts') {
throw new TransformError(format(
UNSUPPORTED_CONFIG_OPTION_ERROR,
name,
'the "@wdio/selenium-standalone-service"',
'https://webdriver.io/docs/selenium-standalone-service#args'
), property.value, this.file)
} else if (name === 'chromeDriver') {
throw new TransformError(format(
UNSUPPORTED_CONFIG_OPTION_ERROR,
name,
'the "wdio-chromedriver-service"',
'https://www.npmjs.com/package/wdio-chromedriver-service'
), property.value, this.file)
} else if (name === 'geckoDriver') {
throw new TransformError(format(
UNSUPPORTED_CONFIG_OPTION_ERROR,
name,
'the "wdio-geckodriver-service"',
'https://www.npmjs.com/package/wdio-geckodriver-service'
), property.value, this.file)
} else if (name === 'sauceProxy') {
throw new TransformError(format(
UNSUPPORTED_CONFIG_OPTION_ERROR,
name,
'the "@wdio/sauce-service"',
'https://webdriver.io/docs/sauce-service#sauceconnect'
), property.value, this.file)
} else if (name === 'sauceBuild') {
throw new TransformError(format(
UNSUPPORTED_CONFIG_OPTION_ERROR,
name,
'custom vendor capabilities, e.g.\n\n' +
'capabilities: [{\n' +
' browserName: "chrome",\n' +
' sauce:options: {\n' +
' build: "My Build #123",\n' +
' name: "My Sauce Labs job"\n' +
' }\n' +
'}]\n\n',
'https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options#TestConfigurationOptions-OptionalSauce-SpecificCapabilitiesforBrowserTests'
), property.value, this.file)
} else if (name === 'firefoxPath') {
throw new TransformError(format(
UNSUPPORTED_CONFIG_OPTION_ERROR,
name,
'custom browser capabilities, e.g.\n\n' +
'capabilities: [{\n' +
' browserName: "firefox",\n' +
' moz:options: {\n' +
' binary: "/path/to/firefox/binary"\n' +
' }\n' +
'}]\n\n',
'https://firefox-source-docs.mozilla.org/testing/geckodriver/Flags.html?highlight=firefoxoptions#code-b-var-binary-var-code-code-binary-var-binary-var-code'
), property.value, this.file)
} else if (name === 'allScriptsTimeout' || name === 'getPageTimeout') {
throw new TransformError(format(
UNSUPPORTED_CONFIG_OPTION_ERROR,
name,
'the "setTimeouts" command to execute within the before hook of your wdio.conf.js',
'https://webdriver.io/docs/api/webdriver#settimeouts'
), property.value, this.file)
} else if (name === 'params') {
throw new TransformError(format(
UNSUPPORTED_CONFIG_OPTION_ERROR,
name,
'environment variables',
'https://webdriver.io/docs/organizingsuites'
), property.value, this.file)
} else if (name === 'resultJsonOutputFile') {
throw new TransformError(format(
UNSUPPORTED_CONFIG_OPTION_ERROR,
name,
'the "wdio-json-reporter"',
'https://www.npmjs.com/package/wdio-json-reporter'
), property.value, this.file)
} else if (name === 'plugins') {
throw new TransformError(format(
UNSUPPORTED_CONFIG_OPTION_ERROR,
name,
'custom services',
'https://webdriver.io/docs/customservices'
), property.value, this.file)
} else if (IGNORED_CONFIG_PROPERTIES.includes(name)) {
return []
}
return property
}
function parseCapabilities (caps) {
const rootLevelConfigs = []
const parsedCaps = []
for (const cap of caps) {
const name = cap.key.name || cap.key.value
if (name === 'name') {
if (!remoteHostname || (!remoteHostname.includes('browserstack') && !remoteHostname.includes('saucelabs'))) {
continue
}
parsedCaps.push(
this.objectProperty(
this.literal('sauce:options'),
this.objectExpression([
this.objectProperty(
this.identifier('name'),
this.literal(cap.value.value)
)
])
)
)
} else if (name === 'seleniumAddress') {
parsedCaps.push(...parseSeleniumAddress.call(this, cap.value.value))
} else if (name === 'chromeOptions') {
parsedCaps.push(
this.objectProperty(
this.literal('goog:chromeOptions'),
cap.value
)
)
} else if (name === 'firefoxOptions') {
parsedCaps.push(
this.objectProperty(
this.literal('moz:firefoxOptions'),
cap.value
)
)
} else if (!IGNORED_CAPABILITIES.includes(name)) {
parsedCaps.push(cap)
}
}
return { rootLevelConfigs, parsedCaps }
}
const ELEM_PROPS = ['length']
const filterElementCalls = ({ value: { argument: { callee: { property: { name } } } } }) => (
ELEMENT_COMMANDS.includes(name) ||
ELEM_PROPS.includes(name)
)
const filterFor = {
argument: { callee: { object: {
type: 'MemberExpression'
} } }
}
function sanitizeAsyncCalls (j, root) {
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(
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,
true
)
)
))
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 } }) => (
j.awaitExpression(
j.callExpression(
j.memberExpression(
j.memberExpression(
j.awaitExpression(argument.callee.object.object),
argument.callee.object.property
),
argument.callee.property
),
argument.arguments
)
)
))
}
function makeAsync ({ value, parentPath }) {
if (
parentPath.parentPath.value.kind === 'constructor' ||
(
parentPath.value.key &&
(
parentPath.value.kind === 'get' ||
parentPath.value.key.name === 'constructor' ||
parentPath.value.key.name.startsWith('async ')
)
)
) {
return value
}
value.async = true
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,
getSelectorArgument,
matchesSelectorExpression,
replaceCommands,
parseConfigProperties,
sanitizeAsyncCalls,
makeAsync,
failAsyncConstructor
}