cypresscypress-custom-commands

Overwrite cypress commands to include a wait before they are run


I am trying to overwrite Cypress commands such as click, type and should to include some waiting time before they are executed. My motivation for this is that I want to highlight the areas the test interacts with in the produced video, so in click I would like to say for example: "Display circle where the click will happen, wait 500ms, click, wait 250ms, remove circle".

The wait-part of this of this is what causes me trouble.

Google suggests I do something like this:

Cypress.Commands.overwrite('click', function (originalFN) {
  const originalParams = [...arguments].slice(1);
  cy.wait(500).then(() => originalFN.apply(originalFN, originalParams));
});

And I think this works for normal clicks(), but it causes the type command to fail entirely saying this: Cypress detected that you returned a promise from a command while also invoking one or more cy commands in that promise.

It seems type() internally calls click in a way that prevents me from using wait() inside click.

Is there any way around this?


Solution

  • I've found a solution in the code of a library to slow down cypress, the key is to overwrite the internal runCommand cypress function. This allows me to do what I want on click and type. Should is still an open question, but not as important. Code below is my function to patch cypress, which I call right before my tests.

    export function patchCypressForVideoRecording(cy: any, Cypress: any, speedFactor = 1) {
      const colorClick = 'rgba(255, 50, 50, 0.8)';
      const colorType = 'rgba(50, 255, 50, 0.8)';
      const colorShould = 'rgba(50, 50, 255, 0.8)';
    
      const waitTime = 600;
    
      const highlightArea = (rect: any, clr: string, scale: boolean) => {
        const x = Math.round(rect.x);
        const y = Math.round(rect.y);
        const w = Math.round(rect.width);
        const h = Math.round(rect.height);
    
        // cy.window() breaks in commands like click due to promise-inside promises stuff
        // this window reference is just there and allows to run synchronous side-effects js without cypress noticing it
        const hackWindow = (cy as any).state('window');
        hackWindow.eval(`
            const time = ${waitTime / speedFactor};
                
            const x = ${x};
            const y = ${y};
        
            const highlightElement = document.createElement('div');
            highlightElement.style.backgroundColor = '${clr}';
            highlightElement.style.position = 'fixed';
            highlightElement.style.zIndex = '999';
            highlightElement.style['pointer-events'] = 'none';
        
            document.body.appendChild(highlightElement);
        
            const scaleElement = (p) => {
                if (${scale}) {
                    const psq = p;
        
                    const scale = (0.1 + ((psq < 0.5 ? (1 - psq) : psq)));
        
                    const w = scale * ${w};
                    const h = scale * ${h};
                    
                    const wLoss = ${w} - w;
                    const hLoss = ${h} - h;
            
                    const x = ${x} + wLoss / 2;
                    const y = ${y} + hLoss / 2;
            
                    return {x, y, w, h};
                } else {
                    const w = ${w};
                    const h = ${h};
                    
                    const x = ${x};
                    const y = ${y};
            
                    return {x, y, w, h};
                }
            };
        
            const newSize = scaleElement(0);
            highlightElement.style.top = newSize.y + 'px';
            highlightElement.style.left = newSize.x + 'px';
            highlightElement.style.width = newSize.w + "px";
            highlightElement.style.height = newSize.h + "px";
        
            const tickSize = 30;
        
            let op = 1;
            let prog = 0;
            const fadeIv = setInterval(() => {
                prog += tickSize;
        
                const p = Math.min(1, prog / time);
        
                let op = 1-(p*0.5);
        
                highlightElement.style.opacity = op + '';
        
                const newSize = scaleElement(p);
                highlightElement.style.top = newSize.y + 'px';
                highlightElement.style.left = newSize.x + 'px';
                highlightElement.style.width = newSize.w + "px";
                highlightElement.style.height = newSize.h + "px";
        
            }, tickSize);
        
            setTimeout(() => {
                clearInterval(fadeIv);
                document.body.removeChild(highlightElement);
            }, time);
          `);
      };
    
      const highlightInteractedElements = (firstParam: any, clr: string, scale: boolean) => {
        if (firstParam != null && firstParam.length != null && firstParam.length > 0 && typeof firstParam !== 'string') {
          for (let i = 0; i < firstParam.length; i++) {
            const elem = firstParam[i];
            if (elem != null && 'getBoundingClientRect' in elem && typeof elem['getBoundingClientRect'] === 'function') {
              highlightArea(elem.getBoundingClientRect(), clr, scale);
            }
          }
        }
      };
    
      // To figure out the element that is clicked/typed in need to wait until
      // the selector right before click/type has a subject element
      const waitAndDisplay = (x: any, clr: string) => {
        if (x.state === 'passed') {
          highlightInteractedElements(x.attributes.subject, clr, true);
        } else {
          if (x.attributes.prev.state === 'queued') {
            setTimeout(() => {
              waitAndDisplay(x, clr);
            }, 15);
          } else {
            highlightInteractedElements(x.attributes.prev.attributes.subject, clr, true);
          }
        }
      };
    
      const cqueue = (cy as any).queue;
      const rc = cqueue.runCommand.bind(cqueue);
    
      cqueue.runCommand = (cmd: any) => {
        let delay = 50;
    
        if (cmd.attributes.name === 'click') {
          waitAndDisplay(cmd, colorClick);
    
          delay = waitTime / 2;
        }
    
        if (cmd.attributes.name === 'type') {
          waitAndDisplay(cmd, colorType);
    
          delay = waitTime;
        }
    
        return Cypress.Promise.delay(delay / speedFactor)
          .then(() => rc(cmd))
          .then(() => Cypress.Promise.delay(delay / speedFactor));
      };
    
      Cypress.Commands.overwrite('should', function (originalFN: any) {
        const originalParams = [...arguments].slice(1);
    
        highlightInteractedElements(originalParams[0], colorShould, false);
    
        return originalFN.apply(originalFN, originalParams);
      });
    }