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.

21e6f3240cb69bbfa80625aa2c21a54c?s=128

Christian Bromann

May 03, 2021
Tweet

Transcript

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

    Transition From Protractor to WebdriverIO
  2. Christian Bromann Open Source Program Office @bromann @christian-bromann

  3. The Types of an Open Source Project

  4. The Types of an Open Source Project Solo

  5. The Types of an Open Source Project Solo Monarchist

  6. The Types of an Open Source Project Solo Monarchist Community

  7. Corporate Foundation

  8. https://github.com/angular/protractor/issues/5502

  9. https://github.com/angular/protractor/issues/5502

  10. Codemods “ Code Modifications

  11. https://www.youtube.com/watch?v=d0pOgY8__JM

  12. 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.
  13. console.log("Hello GermanTestingDays 👋 ") 1

  14. console.log("Hello GermanTestingDays 👋 ") 1 Program

  15. console.log("Hello GermanTestingDays 👋 ") 1 Program CallExpression

  16. console.log("Hello GermanTestingDays 👋 ") 1 Program CallExpression MemberExpression callee

  17. console.log("Hello GermanTestingDays 👋 ") 1 Program CallExpression MemberExpression callee Identifier

    object
  18. console.log("Hello GermanTestingDays 👋 ") 1 Program CallExpression MemberExpression callee Identifier

    object Identifier property
  19. console.log("Hello GermanTestingDays 👋 ") 1 Program CallExpression MemberExpression callee Literal

    arguments Identifier object Identifier property
  20. Why WebdriverIO?

  21. None
  22. None
  23. None
  24. None
  25. None
  26. None
  27. None
  28. $ npm install jscodeshift @wdio/codemod 1 Let's Migrate to 🎉

  29. What happens to all my Angular locators?

  30. 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)
  31. 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)
  32. 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)
  33. 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)
  34. 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)
  35. 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)
  36. 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
  37. But what about waitForAngular ???

  38. 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
  39. 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
  40. 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
  41. DEMO

  42. Thank You! @bromann @christian-bromann