Snippets

Matt Meisberger improved route support for complex usage in cypress

Updated by Matt Meisberger

File improved-router.ts Modified

  • Ignore whitespace
  • Hide word diff
         }
 
         // 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();
+        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]) => {
+Cypress.Commands.overwrite('visit', (orig, ...[one, two]) => {
     // default visit can be structured in a few differnt ways
     const [url, conf] = two === 'object'
         ? [one, two]
Updated by Matt Meisberger

File _readme.md Modified

  • Ignore whitespace
  • Hide word diff
-#### What and Why
-## Why
+### 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
+### What
 This supports doing request based responses or sequence based responses;
 
 
-#### Usage 1 - request body matching
+### 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
 ```typescript
     cy.route('POST', '**/v1/lookup/**', body => body.type === 'TypeA', 'here');
     cy.visit('**/some/url/path');
 ```
 
-#### Usage 1 - sequential results
+### 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
 ```typescript
     cy.route('POST', '**/v1/lookup/**', 'here1', 'here2', 'here3');
     cy.visit('**/some/url/path');
 ```
 
-#### Instalation
+### 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
Created by Matt Meisberger

File _readme.md Added

  • Ignore whitespace
  • Hide word diff
+#### 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
+```typescript
+    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
+```typescript
+    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
+```typescript
+import '../utils/improved-router';
+
+
+```

File improved-router.ts Added

  • Ignore whitespace
  • Hide word diff
+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);
+});
+

File index.d.ts Added

  • Ignore whitespace
  • Hide word diff
+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;
HTTPS SSH

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