From 10485abdbc3522ddf4a8288abccfc701485bf61e Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Fri, 25 Sep 2026 16:19:17 -0700 Subject: [PATCH] feat: add a v10 transform for legacy command signatures Rewrite boolean addCommand and overwriteCommand scopes, getHTML(boolean), and string getCookies or getCookie filters into the v10 options objects. --- README.md | 3 + test/__fixtures__/v10/source/spec.js | 16 +++ test/__fixtures__/v10/transformed/spec.js | 36 +++++++ test/runner.js | 3 + v10/index.js | 117 ++++++++++++++++++++++ 5 files changed, 175 insertions(+) create mode 100644 test/__fixtures__/v10/source/spec.js create mode 100644 test/__fixtures__/v10/transformed/spec.js create mode 100644 v10/index.js diff --git a/README.md b/README.md index dcf8e21..4b37fca 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ This project contains various codemods to help migrating from either one major W - [x] v5 ▶️  v6 (see [migration guide](https://webdriver.io/docs/v6-migration)) - [x] v6 ▶️  v7 (see [migration guide](https://webdriver.io/docs/v7-migration)) +- [x] v9 ▶️  v10 legacy command signatures (see [migration guide](https://webdriver.io/docs/v10-migration#legacy-command-signatures)) - [x] Protractor ▶️  WebdriverIO (see [migration guide](https://webdriver.io/docs/protractor-migration)) - [x] Sync ▶️  Async (see [migration guide](https://webdriver.io/docs/async-migration)) - Note: This codemod depends on the [proxying of chained async calls](https://webdriver.io/blog/2021/07/28/sync-api-deprecation/) added in v7.9 @@ -30,6 +31,8 @@ $ npx jscodeshift -t ./node_modules/@wdio/codemod/ $ npx jscodeshift -t ./node_modules/@wdio/codemod/v6 ./e2e/ # e.g. to migrate from v6 to v7: $ npx jscodeshift -t ./node_modules/@wdio/codemod/v7 ./e2e/ +# e.g. to update v9 command signatures for v10: +$ npx jscodeshift -t ./node_modules/@wdio/codemod/v10 ./e2e/ # e.g. to transform Protractor code: $ npx jscodeshift -t ./node_modules/@wdio/codemod/protractor ./e2e/ # e.g. to tranform from sync to async diff --git a/test/__fixtures__/v10/source/spec.js b/test/__fixtures__/v10/source/spec.js new file mode 100644 index 0000000..6e2af36 --- /dev/null +++ b/test/__fixtures__/v10/source/spec.js @@ -0,0 +1,16 @@ +browser.addCommand('myFn', fn, true) +browser.addCommand('other', fn, false) +browser.addCommand('already', fn, { attachToElement: true }) +browser.overwriteCommand('click', fn, true) +browser.addCommand('withProto', fn, true, proto, instances) + +await $('h1').getHTML(false) +await $('h1').getHTML(true) +await $('h1').getHTML({ includeSelectorTag: false }) + +await browser.getCookies('session') +await browser.getCookie('auth') +await browser.getCookies(['session']) +await browser.getCookies(['session', 'auth']) +await browser.getCookies({ name: 'kept' }) +await browser.getCookies() diff --git a/test/__fixtures__/v10/transformed/spec.js b/test/__fixtures__/v10/transformed/spec.js new file mode 100644 index 0000000..fd18919 --- /dev/null +++ b/test/__fixtures__/v10/transformed/spec.js @@ -0,0 +1,36 @@ +browser.addCommand('myFn', fn, { + attachToElement: true +}) +browser.addCommand('other', fn, { + attachToElement: false +}) +browser.addCommand('already', fn, { attachToElement: true }) +browser.overwriteCommand('click', fn, { + attachToElement: true +}) +browser.addCommand('withProto', fn, { + attachToElement: true, + proto: proto, + instances: instances +}) + +await $('h1').getHTML({ + includeSelectorTag: false +}) +await $('h1').getHTML({ + includeSelectorTag: true +}) +await $('h1').getHTML({ includeSelectorTag: false }) + +await browser.getCookies({ + name: 'session' +}) +await browser.getCookies({ + name: 'auth' +}) +await browser.getCookies({ + name: 'session' +}) +await browser.getCookies(['session', 'auth']) +await browser.getCookies({ name: 'kept' }) +await browser.getCookies() diff --git a/test/runner.js b/test/runner.js index 5865ce2..2c108a6 100644 --- a/test/runner.js +++ b/test/runner.js @@ -32,6 +32,9 @@ const frameworkTests = { ['./spec.js', './spec.js'], ['./compilerFunctions.js', './compilerFunctions.js'] ], + v10: [ + ['./spec.js', './spec.js'] + ], v6: [ ['./spec.js', './spec.js'], ['./conf.js', './conf.js'] diff --git a/v10/index.js b/v10/index.js new file mode 100644 index 0000000..f86cbbe --- /dev/null +++ b/v10/index.js @@ -0,0 +1,117 @@ +/** + * Mechanical v9 → v10 command signature updates: + * - addCommand / overwriteCommand boolean scope → `{ attachToElement }` + * - getHTML(boolean) → getHTML({ includeSelectorTag }) + * - getCookies('name') / getCookies(['name']) / getCookie('name') → getCookies({ name }) + */ +module.exports = function transformer (file, api) { + const j = api.jscodeshift + const root = j(file.source) + + root.find(j.CallExpression).forEach((path) => { + const callee = path.value.callee + const name = memberName(callee) + if (!name) { + return + } + + const args = path.value.arguments + if ((name === 'addCommand' || name === 'overwriteCommand') && isBoolean(args[2])) { + const properties = [ + property(j, 'attachToElement', args[2]) + ] + if (args[3] && !isUndefined(args[3])) { + properties.push(property(j, 'proto', args[3])) + } + if (args[4] && !isUndefined(args[4])) { + properties.push(property(j, 'instances', args[4])) + } + path.value.arguments = [args[0], args[1], j.objectExpression(properties)] + return + } + + if (name === 'getHTML' && args.length === 1 && isBoolean(args[0])) { + path.value.arguments = [ + j.objectExpression([property(j, 'includeSelectorTag', args[0])]) + ] + return + } + + if (name !== 'getCookies' && name !== 'getCookie') { + return + } + + const filter = cookieNameFilter(j, args[0]) + if (!filter) { + return + } + + if (name === 'getCookie') { + renameMember(callee, 'getCookies') + } + args[0] = filter + }) + + return root.toSource() +} + +function memberName (callee) { + if (!callee || !callee.property || callee.computed && !isString(callee.property)) { + return + } + if (callee.property.type === 'Identifier') { + return callee.property.name + } + if (isString(callee.property)) { + return callee.property.value + } +} + +function renameMember (callee, name) { + if (callee.property.type === 'Identifier') { + callee.property.name = name + return + } + callee.property.value = name + if ('extra' in callee.property && callee.property.extra) { + callee.property.extra.raw = `'${name}'` + } +} + +function property (j, key, value) { + return j.property('init', j.identifier(key), value) +} + +function isBoolean (node) { + return Boolean(node) && ( + node.type === 'BooleanLiteral' || + (node.type === 'Literal' && typeof node.value === 'boolean') + ) +} + +function isString (node) { + return Boolean(node) && ( + node.type === 'StringLiteral' || + (node.type === 'Literal' && typeof node.value === 'string') + ) +} + +function isUndefined (node) { + return (node.type === 'Identifier' && node.name === 'undefined') || + (node.type === 'UnaryExpression' && node.operator === 'void') +} + +function cookieNameFilter (j, node) { + if (isString(node)) { + return j.objectExpression([property(j, 'name', node)]) + } + + if ( + node && + node.type === 'ArrayExpression' && + node.elements.length === 1 && + isString(node.elements[0]) + ) { + return j.objectExpression([property(j, 'name', node.elements[0])]) + } +}