Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Let the Code Write Itself - How a Codemod Helps You Transition From Protractor to WebdriverIO

Let the Code Write Itself - How a Codemod Helps You Transition From Protractor to WebdriverIO

Any migration from one framework to another is always painful and often contains a lot of manual effort. With the Angular team announcing the end of Protractor many people are now facing exactly that kind of unnecessary and difficult transition to a new testing environment.

The maintenance of large code bases has been a problem for many developers especially in the JS ecosystem. A proven solution for this has been codemods which is a way to transform code at scale in a totally automated way but with human oversight.

In this talk Christian Bromann will explain how WebdriverIO helps folks to move from Protractor to WebdriverIO using a codemod that can transform thousands of test files with a single command. He will explain how codemods work and demonstrate transitioning an example Protractor project written with page objects into WebdriverIO.

Christian Bromann

May 03, 2021
Tweet

More Decks by Christian Bromann

Other Decks in Programming

Transcript

  1. Let the Code Write Itself How a Codemod Helps You

    Transition From Protractor to WebdriverIO
  2. Abstract Syntax Tree “ In computer science, an abstract syntax

    tree (AST), or just syntax tree, is a tree representation of the abstract syntactic structure of source code written in a programming language.
  3. callee root.find(j.CallExpression, { /** 1 * transform: 2 * element(by.css("..."))

    3 * $('...') 4 */ 5 6 callee: { 7 name: 'element' 8 } 9 }).filter(({ value }) => { 10 const args = value.arguments 11 return ( 12 args.length === 1 && 13 args[0].callee.type === 'MemberExpression' && 14 args[0].callee.object.name === 'by' 15 ) 16 }).replaceWith((path) => { 17 const args = path.value.arguments[0] 18 return j.callExpression( 19 j.identifier('$'), 20 getSelectorArgument( 21 j, path, args, file 22 ) 23 ) 24 }) 25 element(by.css("#elem")) 1 arguments CallExpression CallExpression Literal ME arguments O P (element) (by) (css) (#elem)
  4. callee root.find(j.CallExpression, { /** 1 * transform: 2 * element(by.css("..."))

    3 * $('...') 4 */ 5 6 callee: { 7 name: 'element' 8 } 9 }).filter(({ value }) => { 10 const args = value.arguments 11 return ( 12 args.length === 1 && 13 args[0].callee.type === 'MemberExpression' && 14 args[0].callee.object.name === 'by' 15 ) 16 }).replaceWith((path) => { 17 const args = path.value.arguments[0] 18 return j.callExpression( 19 j.identifier('$'), 20 getSelectorArgument( 21 j, path, args, file 22 ) 23 ) 24 }) 25 callee: { name: 'element' } /** 1 * transform: 2 * element(by.css("...")) 3 * $('...') 4 */ 5 root.find(j.CallExpression, { 6 7 8 9 }).filter(({ value }) => { 10 const args = value.arguments 11 return ( 12 args.length === 1 && 13 args[0].callee.type === 'MemberExpression' && 14 args[0].callee.object.name === 'by' 15 ) 16 }).replaceWith((path) => { 17 const args = path.value.arguments[0] 18 return j.callExpression( 19 j.identifier('$'), 20 getSelectorArgument( 21 j, path, args, file 22 ) 23 ) 24 }) 25 element(by.css("#elem")) 1 arguments CallExpression CallExpression Literal ME arguments O P (element) (by) (css) (#elem)
  5. callee root.find(j.CallExpression, { /** 1 * transform: 2 * element(by.css("..."))

    3 * $('...') 4 */ 5 6 callee: { 7 name: 'element' 8 } 9 }).filter(({ value }) => { 10 const args = value.arguments 11 return ( 12 args.length === 1 && 13 args[0].callee.type === 'MemberExpression' && 14 args[0].callee.object.name === 'by' 15 ) 16 }).replaceWith((path) => { 17 const args = path.value.arguments[0] 18 return j.callExpression( 19 j.identifier('$'), 20 getSelectorArgument( 21 j, path, args, file 22 ) 23 ) 24 }) 25 callee: { name: 'element' } /** 1 * transform: 2 * element(by.css("...")) 3 * $('...') 4 */ 5 root.find(j.CallExpression, { 6 7 8 9 }).filter(({ value }) => { 10 const args = value.arguments 11 return ( 12 args.length === 1 && 13 args[0].callee.type === 'MemberExpression' && 14 args[0].callee.object.name === 'by' 15 ) 16 }).replaceWith((path) => { 17 const args = path.value.arguments[0] 18 return j.callExpression( 19 j.identifier('$'), 20 getSelectorArgument( 21 j, path, args, file 22 ) 23 ) 24 }) 25 return ( args.length === 1 && args[0].callee.type === 'MemberExpression' && args[0].callee.object.name === 'by' ) /** 1 * transform: 2 * element(by.css("...")) 3 * $('...') 4 */ 5 root.find(j.CallExpression, { 6 callee: { 7 name: 'element' 8 } 9 }).filter(({ value }) => { 10 const args = value.arguments 11 12 13 14 15 16 }).replaceWith((path) => { 17 const args = path.value.arguments[0] 18 return j.callExpression( 19 j.identifier('$'), 20 getSelectorArgument( 21 j, path, args, file 22 ) 23 ) 24 }) 25 element(by.css("#elem")) 1 arguments CallExpression CallExpression Literal ME arguments O P (element) (by) (css) (#elem)
  6. callee root.find(j.CallExpression, { /** 1 * transform: 2 * element(by.css("..."))

    3 * $('...') 4 */ 5 6 callee: { 7 name: 'element' 8 } 9 }).filter(({ value }) => { 10 const args = value.arguments 11 return ( 12 args.length === 1 && 13 args[0].callee.type === 'MemberExpression' && 14 args[0].callee.object.name === 'by' 15 ) 16 }).replaceWith((path) => { 17 const args = path.value.arguments[0] 18 return j.callExpression( 19 j.identifier('$'), 20 getSelectorArgument( 21 j, path, args, file 22 ) 23 ) 24 }) 25 callee: { name: 'element' } /** 1 * transform: 2 * element(by.css("...")) 3 * $('...') 4 */ 5 root.find(j.CallExpression, { 6 7 8 9 }).filter(({ value }) => { 10 const args = value.arguments 11 return ( 12 args.length === 1 && 13 args[0].callee.type === 'MemberExpression' && 14 args[0].callee.object.name === 'by' 15 ) 16 }).replaceWith((path) => { 17 const args = path.value.arguments[0] 18 return j.callExpression( 19 j.identifier('$'), 20 getSelectorArgument( 21 j, path, args, file 22 ) 23 ) 24 }) 25 return ( args.length === 1 && args[0].callee.type === 'MemberExpression' && args[0].callee.object.name === 'by' ) /** 1 * transform: 2 * element(by.css("...")) 3 * $('...') 4 */ 5 root.find(j.CallExpression, { 6 callee: { 7 name: 'element' 8 } 9 }).filter(({ value }) => { 10 const args = value.arguments 11 12 13 14 15 16 }).replaceWith((path) => { 17 const args = path.value.arguments[0] 18 return j.callExpression( 19 j.identifier('$'), 20 getSelectorArgument( 21 j, path, args, file 22 ) 23 ) 24 }) 25 return j.callExpression( ) /** 1 * transform: 2 * element(by.css("...")) 3 * $('...') 4 */ 5 root.find(j.CallExpression, { 6 callee: { 7 name: 'element' 8 } 9 }).filter(({ value }) => { 10 const args = value.arguments 11 return ( 12 args.length === 1 && 13 args[0].callee.type === 'MemberExpression' && 14 args[0].callee.object.name === 'by' 15 ) 16 }).replaceWith((path) => { 17 const args = path.value.arguments[0] 18 19 j.identifier('$'), 20 getSelectorArgument( 21 j, path, args, file 22 ) 23 24 }) 25 element(by.css("#elem")) 1 arguments CallExpression CallExpression Literal ME arguments O P (element) (by) (css) (#elem)
  7. callee root.find(j.CallExpression, { /** 1 * transform: 2 * element(by.css("..."))

    3 * $('...') 4 */ 5 6 callee: { 7 name: 'element' 8 } 9 }).filter(({ value }) => { 10 const args = value.arguments 11 return ( 12 args.length === 1 && 13 args[0].callee.type === 'MemberExpression' && 14 args[0].callee.object.name === 'by' 15 ) 16 }).replaceWith((path) => { 17 const args = path.value.arguments[0] 18 return j.callExpression( 19 j.identifier('$'), 20 getSelectorArgument( 21 j, path, args, file 22 ) 23 ) 24 }) 25 callee: { name: 'element' } /** 1 * transform: 2 * element(by.css("...")) 3 * $('...') 4 */ 5 root.find(j.CallExpression, { 6 7 8 9 }).filter(({ value }) => { 10 const args = value.arguments 11 return ( 12 args.length === 1 && 13 args[0].callee.type === 'MemberExpression' && 14 args[0].callee.object.name === 'by' 15 ) 16 }).replaceWith((path) => { 17 const args = path.value.arguments[0] 18 return j.callExpression( 19 j.identifier('$'), 20 getSelectorArgument( 21 j, path, args, file 22 ) 23 ) 24 }) 25 return ( args.length === 1 && args[0].callee.type === 'MemberExpression' && args[0].callee.object.name === 'by' ) /** 1 * transform: 2 * element(by.css("...")) 3 * $('...') 4 */ 5 root.find(j.CallExpression, { 6 callee: { 7 name: 'element' 8 } 9 }).filter(({ value }) => { 10 const args = value.arguments 11 12 13 14 15 16 }).replaceWith((path) => { 17 const args = path.value.arguments[0] 18 return j.callExpression( 19 j.identifier('$'), 20 getSelectorArgument( 21 j, path, args, file 22 ) 23 ) 24 }) 25 return j.callExpression( ) /** 1 * transform: 2 * element(by.css("...")) 3 * $('...') 4 */ 5 root.find(j.CallExpression, { 6 callee: { 7 name: 'element' 8 } 9 }).filter(({ value }) => { 10 const args = value.arguments 11 return ( 12 args.length === 1 && 13 args[0].callee.type === 'MemberExpression' && 14 args[0].callee.object.name === 'by' 15 ) 16 }).replaceWith((path) => { 17 const args = path.value.arguments[0] 18 19 j.identifier('$'), 20 getSelectorArgument( 21 j, path, args, file 22 ) 23 24 }) 25 j.identifier('$'), /** 1 * transform: 2 * element(by.css("...")) 3 * $('...') 4 */ 5 root.find(j.CallExpression, { 6 callee: { 7 name: 'element' 8 } 9 }).filter(({ value }) => { 10 const args = value.arguments 11 return ( 12 args.length === 1 && 13 args[0].callee.type === 'MemberExpression' && 14 args[0].callee.object.name === 'by' 15 ) 16 }).replaceWith((path) => { 17 const args = path.value.arguments[0] 18 return j.callExpression( 19 20 getSelectorArgument( 21 j, path, args, file 22 ) 23 ) 24 }) 25 element(by.css("#elem")) 1 arguments CallExpression CallExpression Literal ME arguments O P (element) (by) (css) (#elem)
  8. callee root.find(j.CallExpression, { /** 1 * transform: 2 * element(by.css("..."))

    3 * $('...') 4 */ 5 6 callee: { 7 name: 'element' 8 } 9 }).filter(({ value }) => { 10 const args = value.arguments 11 return ( 12 args.length === 1 && 13 args[0].callee.type === 'MemberExpression' && 14 args[0].callee.object.name === 'by' 15 ) 16 }).replaceWith((path) => { 17 const args = path.value.arguments[0] 18 return j.callExpression( 19 j.identifier('$'), 20 getSelectorArgument( 21 j, path, args, file 22 ) 23 ) 24 }) 25 callee: { name: 'element' } /** 1 * transform: 2 * element(by.css("...")) 3 * $('...') 4 */ 5 root.find(j.CallExpression, { 6 7 8 9 }).filter(({ value }) => { 10 const args = value.arguments 11 return ( 12 args.length === 1 && 13 args[0].callee.type === 'MemberExpression' && 14 args[0].callee.object.name === 'by' 15 ) 16 }).replaceWith((path) => { 17 const args = path.value.arguments[0] 18 return j.callExpression( 19 j.identifier('$'), 20 getSelectorArgument( 21 j, path, args, file 22 ) 23 ) 24 }) 25 return ( args.length === 1 && args[0].callee.type === 'MemberExpression' && args[0].callee.object.name === 'by' ) /** 1 * transform: 2 * element(by.css("...")) 3 * $('...') 4 */ 5 root.find(j.CallExpression, { 6 callee: { 7 name: 'element' 8 } 9 }).filter(({ value }) => { 10 const args = value.arguments 11 12 13 14 15 16 }).replaceWith((path) => { 17 const args = path.value.arguments[0] 18 return j.callExpression( 19 j.identifier('$'), 20 getSelectorArgument( 21 j, path, args, file 22 ) 23 ) 24 }) 25 return j.callExpression( ) /** 1 * transform: 2 * element(by.css("...")) 3 * $('...') 4 */ 5 root.find(j.CallExpression, { 6 callee: { 7 name: 'element' 8 } 9 }).filter(({ value }) => { 10 const args = value.arguments 11 return ( 12 args.length === 1 && 13 args[0].callee.type === 'MemberExpression' && 14 args[0].callee.object.name === 'by' 15 ) 16 }).replaceWith((path) => { 17 const args = path.value.arguments[0] 18 19 j.identifier('$'), 20 getSelectorArgument( 21 j, path, args, file 22 ) 23 24 }) 25 j.identifier('$'), /** 1 * transform: 2 * element(by.css("...")) 3 * $('...') 4 */ 5 root.find(j.CallExpression, { 6 callee: { 7 name: 'element' 8 } 9 }).filter(({ value }) => { 10 const args = value.arguments 11 return ( 12 args.length === 1 && 13 args[0].callee.type === 'MemberExpression' && 14 args[0].callee.object.name === 'by' 15 ) 16 }).replaceWith((path) => { 17 const args = path.value.arguments[0] 18 return j.callExpression( 19 20 getSelectorArgument( 21 j, path, args, file 22 ) 23 ) 24 }) 25 getSelectorArgument( j, path, args, file ) /** 1 * transform: 2 * element(by.css("...")) 3 * $('...') 4 */ 5 root.find(j.CallExpression, { 6 callee: { 7 name: 'element' 8 } 9 }).filter(({ value }) => { 10 const args = value.arguments 11 return ( 12 args.length === 1 && 13 args[0].callee.type === 'MemberExpression' && 14 args[0].callee.object.name === 'by' 15 ) 16 }).replaceWith((path) => { 17 const args = path.value.arguments[0] 18 return j.callExpression( 19 j.identifier('$'), 20 21 22 23 ) 24 }) 25 element(by.css("#elem")) 1 arguments CallExpression CallExpression Literal ME arguments O P (element) (by) (css) (#elem)
  9. const loc = "selector"; element(by.id(loc)); element(by.xpath(loc)); element(by.name(loc)); element(by.model(loc)); element(by.repeater(loc)); element(by.linkText(loc));

    element(by.partialLinkText(loc)); element(by.className(loc)); element(by.tagName(loc)); element(by.options(loc)); element(by.buttonText(loc)); element(by.partialButtonText(loc)); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 const loc = "selector"; $(`#${loc}`); $(loc); $(`*[name="${loc}"]`); $(`*[ng-model="${loc}"]`); $(`*[ng-repeat="${loc}"]`); $(`=${loc}`); $(`*=${loc}`); $(`.${loc}`); $(loc); $(`select[ng-options="${loc}"] option`); $(`button=${loc}`); $(`button*=${loc}`); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Before After
  10. MemberExpression expression.callee || /** 1 * remove command statements that

    aren't 2 * useful in WebdriverIO world, e.g. 3 * 4 * await $('body').allowAnimations(false); 5 * browser.waitForAngularEnabled(true) 6 */ 7 root.find(j.ExpressionStatement) 8 .filter(({ value: { expression } }) => { 9 const expr = ( 10 11 ( 12 expression.argument && 13 expression.argument.callee 14 ) 15 ) 16 return (expr && expr.property && 17 COMMANDS_TO_REMOVE.includes( 18 expr.property.name 19 ) 20 ) 21 }) 22 .remove() 23 browser.waitForAngularEnabled(true) 1 arguments CallExpression Identifier Identifier (browser) (waitForAngularEnabled) (true) object property
  11. MemberExpression expression.callee || /** 1 * remove command statements that

    aren't 2 * useful in WebdriverIO world, e.g. 3 * 4 * await $('body').allowAnimations(false); 5 * browser.waitForAngularEnabled(true) 6 */ 7 root.find(j.ExpressionStatement) 8 .filter(({ value: { expression } }) => { 9 const expr = ( 10 11 ( 12 expression.argument && 13 expression.argument.callee 14 ) 15 ) 16 return (expr && expr.property && 17 COMMANDS_TO_REMOVE.includes( 18 expr.property.name 19 ) 20 ) 21 }) 22 .remove() 23 return (expr && expr.property && COMMANDS_TO_REMOVE.includes( expr.property.name ) ) /** 1 * remove command statements that aren't 2 * useful in WebdriverIO world, e.g. 3 * 4 * await $('body').allowAnimations(false); 5 * browser.waitForAngularEnabled(true) 6 */ 7 root.find(j.ExpressionStatement) 8 .filter(({ value: { expression } }) => { 9 const expr = ( 10 expression.callee || 11 ( 12 expression.argument && 13 expression.argument.callee 14 ) 15 ) 16 17 18 19 20 21 }) 22 .remove() 23 browser.waitForAngularEnabled(true) 1 arguments CallExpression Identifier Identifier (browser) (waitForAngularEnabled) (true) object property
  12. MemberExpression expression.callee || /** 1 * remove command statements that

    aren't 2 * useful in WebdriverIO world, e.g. 3 * 4 * await $('body').allowAnimations(false); 5 * browser.waitForAngularEnabled(true) 6 */ 7 root.find(j.ExpressionStatement) 8 .filter(({ value: { expression } }) => { 9 const expr = ( 10 11 ( 12 expression.argument && 13 expression.argument.callee 14 ) 15 ) 16 return (expr && expr.property && 17 COMMANDS_TO_REMOVE.includes( 18 expr.property.name 19 ) 20 ) 21 }) 22 .remove() 23 return (expr && expr.property && COMMANDS_TO_REMOVE.includes( expr.property.name ) ) /** 1 * remove command statements that aren't 2 * useful in WebdriverIO world, e.g. 3 * 4 * await $('body').allowAnimations(false); 5 * browser.waitForAngularEnabled(true) 6 */ 7 root.find(j.ExpressionStatement) 8 .filter(({ value: { expression } }) => { 9 const expr = ( 10 expression.callee || 11 ( 12 expression.argument && 13 expression.argument.callee 14 ) 15 ) 16 17 18 19 20 21 }) 22 .remove() 23 .remove() /** 1 * remove command statements that aren't 2 * useful in WebdriverIO world, e.g. 3 * 4 * await $('body').allowAnimations(false); 5 * browser.waitForAngularEnabled(true) 6 */ 7 root.find(j.ExpressionStatement) 8 .filter(({ value: { expression } }) => { 9 const expr = ( 10 expression.callee || 11 ( 12 expression.argument && 13 expression.argument.callee 14 ) 15 ) 16 return (expr && expr.property && 17 COMMANDS_TO_REMOVE.includes( 18 expr.property.name 19 ) 20 ) 21 }) 22 23 browser.waitForAngularEnabled(true) 1 arguments CallExpression Identifier Identifier (browser) (waitForAngularEnabled) (true) object property