UNPKG

@revoloo/cypress6

Version:

Cypress.io end to end testing tool

201 lines (165 loc) 5.95 kB
import { Visitor, namedTypes as n, builders as b, } from 'ast-types' import { ExpressionKind } from 'ast-types/gen/kinds' // use `globalThis` instead of `window`, `self`... to lower chances of scope conflict // users can technically override even this, but it would be very rude // "[globalThis] provides a way for polyfills/shims, build tools, and portable code to have a reliable non-eval means to access the global..." // @see https://github.com/tc39/proposal-global/blob/master/NAMING.md const globalIdentifier = b.identifier('globalThis') /** * Generate a CallExpression for Cypress.resolveWindowReference * @param accessedObject object being accessed * @param prop name of property being accessed * @param maybeVal if an assignment is being made, this is the RHS of the assignment */ function resolveWindowReference (accessedObject: ExpressionKind, prop: string, maybeVal?: ExpressionKind) { const args = [ globalIdentifier, accessedObject, b.stringLiteral(prop), ] if (maybeVal) { args.push(maybeVal) } return b.callExpression( b.memberExpression( b.memberExpression( b.memberExpression( globalIdentifier, b.identifier('top'), ), b.identifier('Cypress'), ), b.identifier('resolveWindowReference'), ), args, ) } /** * Generate a CallExpression for Cypress.resolveLocationReference */ function resolveLocationReference () { return b.callExpression( b.memberExpression( b.memberExpression( b.memberExpression( globalIdentifier, b.identifier('top'), ), b.identifier('Cypress'), ), b.identifier('resolveLocationReference'), ), [globalIdentifier], ) } /** * Given an Identifier or a Literal, return a property name that should use `resolveWindowReference`. * @param node */ function getReplaceablePropOfMemberExpression (node: n.MemberExpression) { const { property } = node // something.(top|parent) if (n.Identifier.check(property) && ['parent', 'top', 'location'].includes(property.name)) { return property.name } // something['(top|parent)'] if (n.Literal.check(property) && ['parent', 'top', 'location'].includes(String(property.value))) { return String(property.value) } // NOTE: cases where a variable is used for the prop will not be replaced // for example, `bar = 'top'; window[bar];` will not be replaced // this would most likely be too slow return } /** * An AST Visitor that applies JS transformations required for Cypress. * @see https://github.com/benjamn/ast-types#ast-traversal for details on how the Visitor is implemented * @see https://astexplorer.net/#/gist/7f1e645c74df845b0e1f814454e9bbdf/f443b701b53bf17fbbf40e9285cb8b65a4066240 * to explore ASTs generated by recast */ export const jsRules: Visitor<{}> = { // replace member accesses like foo['top'] or bar.parent with resolveWindowReference visitMemberExpression (path) { const { node } = path const prop = getReplaceablePropOfMemberExpression(node) if (!prop) { return this.traverse(path) } path.replace(resolveWindowReference(path.get('object').node, prop)) return false }, // replace lone identifiers like `top`, `parent`, with resolveWindowReference visitIdentifier (path) { const { node } = path if (path.parentPath) { const parentNode = path.parentPath.node // like `identifer = 'foo'` const isAssignee = n.AssignmentExpression.check(parentNode) && parentNode.left === node if (isAssignee && node.name === 'location') { // `location = 'something'`, rewrite to intercepted href setter since relative urls can break this path.replace(b.memberExpression(resolveLocationReference(), b.identifier('href'))) return false } // some Identifiers do not refer to a scoped variable, depending on how they're used if ( // like `var top = 'foo'` (n.VariableDeclarator.check(parentNode) && parentNode.id === node) || (isAssignee) || ( [ 'LabeledStatement', // like `top: foo();` 'ContinueStatement', // like 'continue top' 'BreakStatement', // like 'break top' 'Property', // like `{ top: 'foo' }` 'FunctionDeclaration', // like `function top()` 'RestElement', // like (...top) 'ArrowFunctionExpression', // like `(top, ...parent) => { }` 'ArrowExpression', // MDN Parser docs mention this being used for () => {} 'FunctionExpression', // like `(function top())`, ].includes(parentNode.type) ) ) { return false } } if (path.scope.declares(node.name)) { // identifier has been declared in local scope, don't care about replacing return this.traverse(path) } if (node.name === 'location') { path.replace(resolveLocationReference()) return false } if (['parent', 'top'].includes(node.name)) { path.replace(resolveWindowReference(globalIdentifier, node.name)) return false } this.traverse(path) }, visitAssignmentExpression (path) { const { node } = path const finish = () => { this.traverse(path) } if (!n.MemberExpression.check(node.left)) { return finish() } const propBeingSet = getReplaceablePropOfMemberExpression(node.left) if (!propBeingSet) { return finish() } if (node.operator !== '=') { // in the case of +=, -=, |=, etc., assume they're not doing something like // `window.top += 4` since that would be invalid anyways, just continue down the RHS this.traverse(path.get('right')) return false } const objBeingSetOn = node.left.object path.replace(resolveWindowReference(objBeingSetOn, propBeingSet, node.right)) return false }, }