Last active
June 24, 2026 19:21
-
-
Save tschaub/a64d7bd5ca377cff2a4dbf63391c3e69 to your computer and use it in GitHub Desktop.
Replace the use of expect.js with assert from chai.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * A transform for use with jscodeshift that replaces expect.js assertions with | |
| * assert style assertions from Chai. | |
| * | |
| * Example use on a directory: | |
| * | |
| * git checkout -- test && npx jscodeshift --transform use-chai.js --extensions js test | |
| * | |
| */ | |
| module.exports = function (info, api) { | |
| const j = api.jscodeshift; | |
| const root = j(info.source); | |
| let modified = false; | |
| function assertCall(method, args) { | |
| return j.callExpression( | |
| j.memberExpression(j.identifier('assert'), j.identifier(method)), | |
| args, | |
| ); | |
| } | |
| function assertStmt(method, args) { | |
| return j.expressionStatement(assertCall(method, args)); | |
| } | |
| function getExpectCall(node) { | |
| while (node && node.type === 'MemberExpression') { | |
| node = node.object; | |
| } | |
| if ( | |
| node && | |
| node.type === 'CallExpression' && | |
| node.callee.type === 'Identifier' && | |
| node.callee.name === 'expect' | |
| ) { | |
| return node; | |
| } | |
| return null; | |
| } | |
| function getChain(node) { | |
| const chain = []; | |
| let current = node; | |
| while (current && current.type === 'MemberExpression') { | |
| chain.unshift(current.property.name); | |
| current = current.object; | |
| } | |
| return chain; | |
| } | |
| function getActual(node) { | |
| const expectCall = getExpectCall(node); | |
| return expectCall ? expectCall.arguments[0] : null; | |
| } | |
| function transformExpectCallExpression(expr) { | |
| const actual = getActual(expr.callee); | |
| if (!actual) { | |
| if ( | |
| expr.callee.type === 'Identifier' && | |
| expr.callee.name === 'expect' && | |
| expr.arguments.length === 1 | |
| ) { | |
| return assertCall('ok', [expr.arguments[0]]); | |
| } | |
| return null; | |
| } | |
| const chain = getChain(expr.callee); | |
| const key = chain.join('.'); | |
| const args = expr.arguments; | |
| switch (key) { | |
| case 'to.be': | |
| return args.length === 1 | |
| ? assertCall('strictEqual', [actual, args[0]]) | |
| : null; | |
| case 'to.be.ok': | |
| return assertCall('ok', [actual]); | |
| case 'to.not.be.ok': | |
| case 'not.to.be.ok': | |
| return assertCall('notOk', [actual]); | |
| case 'to.equal': | |
| return assertCall('equal', [actual, args[0]]); | |
| case 'to.eql': | |
| case 'eql': | |
| return assertCall('deepEqual', [actual, args[0]]); | |
| case 'to.be.eql': | |
| return assertCall('deepEqual', [actual, args[0]]); | |
| case 'to.be.an': | |
| case 'to.be.a': | |
| case 'to.an': | |
| case 'to.a': | |
| if ( | |
| args[0] && | |
| (args[0].type === 'Literal' || args[0].type === 'StringLiteral') | |
| ) { | |
| const typeChecks = { | |
| array: 'isArray', | |
| object: 'isObject', | |
| string: 'isString', | |
| number: 'isNumber', | |
| function: 'isFunction', | |
| boolean: 'isBoolean', | |
| }; | |
| const method = typeChecks[args[0].value]; | |
| if (method) { | |
| return assertCall(method, [actual]); | |
| } | |
| } | |
| return assertCall('instanceOf', [actual, args[0]]); | |
| case 'not.to.be.a': | |
| return assertCall('notInstanceOf', [actual, args[0]]); | |
| case 'not.to.be': | |
| return args.length === 1 | |
| ? assertCall('notEqual', [actual, args[0]]) | |
| : null; | |
| case 'to.not.be': | |
| return args.length === 0 | |
| ? assertCall('isFalse', [actual]) | |
| : assertCall('notEqual', [actual, args[0]]); | |
| case 'to.roughlyEqual': | |
| case 'roughlyEqual': | |
| return assertCall('approximately', [actual, args[0], args[1]]); | |
| case 'not.to.roughlyEqual': | |
| return assertCall('notApproximately', [actual, args[0], args[1]]); | |
| case 'to.have.length': | |
| case 'to.length': | |
| case 'have.length': | |
| return assertCall('lengthOf', [actual, args[0]]); | |
| case 'to.be.empty': | |
| return assertCall('isEmpty', [actual]); | |
| case 'to.not.be.empty': | |
| return assertCall('isNotEmpty', [actual]); | |
| case 'to.throwException': | |
| case 'to.throwError': | |
| return assertCall( | |
| 'throws', | |
| args.length > 0 ? [actual, args[0]] : [actual], | |
| ); | |
| case 'to.not.throwException': | |
| case 'to.not.throwError': | |
| case 'not.to.throwException': | |
| case 'not.to.throwError': | |
| return assertCall( | |
| 'doesNotThrow', | |
| args.length > 0 ? [actual, args[0]] : [actual], | |
| ); | |
| case 'to.not.equal': | |
| return assertCall('notStrictEqual', [actual, args[0]]); | |
| case 'to.be.greaterThan': | |
| case 'to.greaterThan': | |
| case 'to.be.above': | |
| return assertCall('isAbove', [actual, args[0]]); | |
| case 'to.be.lessThan': | |
| case 'to.be.below': | |
| case 'to.below': | |
| return assertCall('isBelow', [actual, args[0]]); | |
| case 'to.be.within': | |
| return assertCall('isWithin', [actual, args[0], args[1]]); | |
| case 'to.xmleql': | |
| return assertCall( | |
| 'xmlEqual', | |
| args.length > 1 ? [actual, args[0], args[1]] : [actual, args[0]], | |
| ); | |
| case 'to.not.xmleql': | |
| return assertCall( | |
| 'notXmlEqual', | |
| args.length > 1 ? [actual, args[0], args[1]] : [actual, args[0]], | |
| ); | |
| case 'to.contain': | |
| case 'contain': | |
| return assertCall('include', [actual, args[0]]); | |
| case 'to.not.contain': | |
| case 'not.to.contain': | |
| case 'not.contain': | |
| return assertCall('notInclude', [actual, args[0]]); | |
| case 'to.not.eql': | |
| case 'not.to.eql': | |
| return assertCall('notDeepEqual', [actual, args[0]]); | |
| case 'to.have.property': | |
| return args.length === 2 | |
| ? assertCall('propertyVal', [actual, args[0], args[1]]) | |
| : assertCall('property', [actual, args[0]]); | |
| case 'to.not.have.property': | |
| return assertCall('notProperty', [actual, args[0]]); | |
| case 'to.have.key': | |
| return assertCall('property', [actual, args[0]]); | |
| case 'to.only.have.key': | |
| return assertCall('hasAllKeys', [actual, j.arrayExpression(args)]); | |
| case 'to.only.have.keys': | |
| return assertCall('hasAllKeys', [actual, args[0]]); | |
| case 'to.match': | |
| return assertCall('match', [actual, args[0]]); | |
| case 'fail': | |
| return assertCall('fail', []); | |
| default: | |
| return null; | |
| } | |
| } | |
| function transformExpectMemberExpression(expr) { | |
| const actual = getActual(expr); | |
| if (!actual) { | |
| return null; | |
| } | |
| const chain = getChain(expr); | |
| const key = chain.join('.'); | |
| switch (key) { | |
| case 'to.be.null': | |
| return assertCall('isNull', [actual]); | |
| default: | |
| return null; | |
| } | |
| } | |
| function applyTransform(path, replacement) { | |
| if (!replacement) { | |
| return; | |
| } | |
| if (path.value.type === 'ExpressionStatement') { | |
| j(path).replaceWith(j.expressionStatement(replacement)); | |
| } else { | |
| j(path).replaceWith(replacement); | |
| } | |
| modified = true; | |
| } | |
| root.find(j.ExpressionStatement).forEach((path) => { | |
| const expr = path.value.expression; | |
| if (expr.type === 'CallExpression') { | |
| applyTransform(path, transformExpectCallExpression(expr)); | |
| } else if (expr.type === 'MemberExpression') { | |
| applyTransform(path, transformExpectMemberExpression(expr)); | |
| } | |
| }); | |
| root.find(j.ArrowFunctionExpression).forEach((path) => { | |
| if (!path.value.expression) { | |
| return; | |
| } | |
| const body = path.value.body; | |
| if (body.type === 'CallExpression') { | |
| const replacement = transformExpectCallExpression(body); | |
| if (replacement) { | |
| path.value.body = replacement; | |
| modified = true; | |
| } | |
| } else if (body.type === 'MemberExpression') { | |
| const replacement = transformExpectMemberExpression(body); | |
| if (replacement) { | |
| path.value.body = replacement; | |
| modified = true; | |
| } | |
| } | |
| }); | |
| root.find(j.ImportDeclaration).forEach((path) => { | |
| const source = path.value.source.value; | |
| if (typeof source === 'string' && source.endsWith('expect.js')) { | |
| j(path).remove(); | |
| modified = true; | |
| } | |
| }); | |
| const hasChaiImport = | |
| root.find(j.ImportDeclaration, { | |
| source: {value: 'chai'}, | |
| }).length > 0; | |
| const usesAssert = | |
| modified || | |
| hasChaiImport || | |
| root.find(j.MemberExpression, { | |
| object: {type: 'Identifier', name: 'assert'}, | |
| }).length > 0; | |
| if (usesAssert && !hasChaiImport) { | |
| const body = root.find(j.Program).get('body'); | |
| body.unshift( | |
| j.importDeclaration( | |
| [j.importSpecifier(j.identifier('assert'))], | |
| j.literal('chai'), | |
| ), | |
| ); | |
| modified = true; | |
| } | |
| if (!modified) { | |
| return null; | |
| } | |
| return root.toSource({quote: 'single'}); | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment