Search code examples
typescripttimeoutcucumberjs

Cucumber JS - Is it possible to programatically set defaultTimeout value in setDefinitionFunctionWrapper?


Cucumber out of the box provides a method to set a default timeout per step, or a 'global' timeout that all steps will use through the use of setDefaultTimeout(value).

However, there is no way to have a scenario level timeout, i.e. the entire scenario should time out after X milliseconds.

I am in the situation where I need to ensure that any given scenario will not run for more than X minutes, but I also need the process to end cleanly with any After hooks running as if the test had failed. My idea was to do something like this:

const MAX_DURATION = 2 * 60 * 1000;
let startTime: number | undefined;
let endTime: number | undefined;

setDefinitionFunctionWrapper(function(func: Function) {
  return async function(this: any, ...args: any[]) {
    if(startTime && endTime) {
      setDefaultTimeout(Math.max(endTime - Date.now(), 0));
    }

    return await func.apply(this, args);
  };
});

Before(() => {
  startTime = Date.now();
  endTime = startTime + 1000;
});

The theory was that before each scenario, I'd grab the time now, work out the time at which it would timeout, then before each step set the default timeout to be that endTime value minus the time now.

This does not work though. I'm assuming that when the steps are built in cucumber lib, timeouts are worked out on a per step level and cannot be changed.

I cannot simply use a setTimeout and throw an error as this results in an unhandled promise exception and Cucumber just exits.

Has anyone tried something similar, or have any ideas of how this could be achieved?


Solution

  • So I rethought my approach and something like this seems to do the trick:

    setDefinitionFunctionWrapper(function (func: Function) {
      return async function (this: any, ...args: []) {
        if (!maxEndTime) return await func.apply(this, args);
    
        let id: NodeJS.Timeout | undefined;
        const execution = func.apply(this, args);
        const timeout = new Promise((resolve) => {
          id = global.setTimeout(() => {
            hasTimedOut = true;
            resolve('skipped');
          }, Math.max(maxEndTime - Date.now(), 0));
        });
    
        const res = await Promise.race([execution, timeout]);
        if (id) clearTimeout(id);
    
        return res;
      };
    });
    

    This creates two promises - one that resolves after a specified timeout, and one one the test step completes. I then race them and whichever resolves first wins!

    I then handle the hasTimedOut value elsewhere to do the appropriate things for that result.