Snippets

Matt Meisberger bodyRouter

Created by Matt Meisberger last modified
import { ECHO_BACK_URL } from "./env/shared";

const visitConf = {
    onBeforeLoad: (cw: any) => {
        cw.XMLHttpRequest.prototype.open = getNewXhrOpen(cw.XMLHttpRequest.prototype.open)
        cw.XMLHttpRequest.prototype.send = getNewSender(cw.XMLHttpRequest.prototype.send);
    }
}

const getNewSender = (oldSend: any) => function(this: any, body: any) {
    // get the body mapper or an identity fn
    const newBody = (this.bodyMappers as any[])
        .map(m => m(body))
        .filter(doesBodyMatch)
        .map(stringify)
        .pop();

    return oldSend.bind(this)(
    newBody || body
    );
}

const getNewXhrOpen = (oldOpen: any) => function(this: any, method: any, url: any) {
    this.bodyMappers = (cy.routeBodyMatchers || [])
        .map(matcher => matcher(method, url))
        .filter(Boolean);

    return oldOpen.bind(this)(
        method,
        this.bodyMappers.length ? ECHO_BACK_URL : url,
        true
    );
}

const doesBodyMatch = (res: any) => res !== '__NO_MATCH__';
const stringify = (v: any) => JSON.stringify(v);
const parseOrValue = (v: any) => {
    try {
        return JSON.parse(v);
    } catch (e) {
        return v;
    }
}
const safeFn = (fn: ((...args: any[]) => any)) => (...args: any[]): any => {
    try { return fn(...args); } catch (e) { console.log('caught and ignore')}
}

const routeBodyCommand = (method: string, url: string, tester: (body:any) => boolean, response: any, ...responses: any[]) => {
    let allResponses = [response, ...responses];

    tester = safeFn(tester);

    // make a mapper to determine if we should replace the body
    let routeToBodyMapper = (method2: string, fullUrl: string) => {
        if (method2.toUpperCase() !== method.toUpperCase()) {
            return false;
        }

        if (!fullUrl.match(new RegExp(url.split('**').join('.+?'), 'i'))) {
            return false;
        }

        // this __NO_MATCH__ is so that the user can force false or undefined or whatever they want though
        return (body: any) => console.log('compare', allResponses, url, body) as any || (!tester(parseOrValue(body)) ? '__NO_MATCH__' : allResponses.length ? allResponses.shift() : response);
    }

    // registerUrlBodyMatch(method, url, tester, response);
    cy.routeBodyMatchers = (cy.routeBodyMatchers || []).concat(routeToBodyMapper);
}

Cypress.Commands.overwrite('visit', (orig, ...args) => {
    const [one, two] = args;

    let config;
    let url;
    if (typeof two === 'object') {
        url = one;
       config = {
           ...two,
           ...visitConf
       }
    } else if(typeof one === 'object') {
        url = one.url;
        config = {
            ...one,
            ...visitConf
        }
    } else {
        url = one;
        config = visitConf;
    }

    return orig(url, config);
});

Cypress.Commands.overwrite('route', (orig, ...args: any[]) => {
    if (args.length < 4) return orig(...args);

    const [method, url, maybeFnMaybeResponse, ...responses] = args;

    const fn = typeof maybeFnMaybeResponse === 'function'
        ? maybeFnMaybeResponse
        : () => true; // we are in subsequent call mode so all bodies match

    const allResponses = typeof maybeFnMaybeResponse === 'function'
        ? responses
        : [maybeFnMaybeResponse, ...responses]

        return (routeBodyCommand as any)(method, url, fn, ...allResponses);
});

declare namespace Cypress {
    interface Chainable {
      routeBodyMatchers: routeMatcher[]|undefined;
      route<T = any>(method: string, url: string, tester: (body: T) => boolean, response: any): void
      route<T = any>(method: string, url: string, response: any, ...responses: any[]): void
    }
}

type bodyMatcher = (body: any) => any|'__NO_MATCH__';
type routeMatcher = (method: string, url: string) => bodyMatcher|false;

What and Why

This adds support for having multiple url routes based on body. Cypress doesn't currently support so a single URL can have only one return value.

Usage 1 - body matching

this will set a watcher on the url and return 'here' but only if the body validate fn returns true when passed the request body

    cy.routeBody('POST', '**/v1/lookup/**', body => body.some(propEq('value', 'serviceType')), 'here');
    cy.visitPage('**/some/url/path');

Usage 1 - sequential results

this will watch the url and will reply with here1 on the first ajax, here2 on the second, here3 on the third and is variadic so it will support as many responses as you need

    cy.routeBody('POST', '**/v1/lookup/**', 'here1', 'here2', 'here3');
    cy.visitPage('**/some/url/path');

Instalation

you must have a url that will echo back whatever body is sent to it and export that as ECHO_BACK_URL which is imported by bodyRouter.ts copy the included files to cypress/utils in cypress/support/command.ts import the bodyBodyRouter.ts file

import '../utils/bodyRouter';

Comments (0)

HTTPS SSH

You can clone a snippet to your computer for local editing. Learn more.