Snippets

Matt Meisberger improved route support for complex usage in cypress

Created by Matt Meisberger last modified

What and Why

Why

Cypress does not support mocking an endpoint based on the request body for example a search endpoint that returns different results based on the query passed. Also not supported would be a counter endpoint as the mock would always return the same result.

What

This supports doing request based responses or sequence based responses;

Usage 1 - request 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.route('POST', '**/v1/lookup/**', body => body.type === 'TypeA', 'here');
    cy.route('POST', '**/v1/lookup/**', body => body.type === 'TypeB', 'there');

    cy.visit('**/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.route('POST', '**/v1/lookup/**', 'here1', 'here2', 'here3');
    cy.route('GET', '**/some/counter/**', ...range(1, 10000)) // counter with support up to 10,000
    cy.visit('**/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/improved-router';
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) { }
}

const improvedRouterCommandHandler = (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) => !allResponses.length || !tester(parseOrValue(body)) ? '__NO_MATCH__' : allResponses.shift();
    }

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

Cypress.Commands.overwrite('visit', (orig, ...[one, two]) => {
    // default visit can be structured in a few differnt ways
    const [url, conf] = two === 'object'
        ? [one, two]
        : one === 'object'
            ? [one.url, one]
            : [one, {}];

    return orig(url, {...conf, ...visitConf});
});

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 (improvedRouterCommandHandler 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;

Comments (1)

  1. Zafer özkel
HTTPS SSH

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