Jest unit test: how mock fetch() with abort signal

I have the following typescript function that

  1. tries to fetch the content of a URL,
  2. should time out after certain time with AbortController.

Now I want to write a unit test that simulates the timeout behavior: controller.signal should be called. I should expect an “AbortError” to be thrown.

export async function fetchScriptContents(
    scriptUrl: string,
    timeout?: number
): Promise<string> {
    const controller = new AbortController();
    const fetchTimeout = setTimeout(() => {
        controller.abort(`AbortError: Fetch timeout after ${timeout} ms`);
    }, timeout ?? 3000);

    return await fetch(scriptUrl, { signal: controller.signal, cache: "no-cache" })
        .then(async (response: Response) => {
            //do something
         }
        .catch(async (e: Error) => {
            clearTimeout(fetchTimeout);
            if (e.name === "TimeoutError") {
                throw new Error(`fetchScriptContents: timeout error fetching script ${scriptUrl}`);
            } else if (e.name === "AbortError") {   
                throw new Error(`fetchScriptContents: Aborted by user action or fetch timeout: ${e.message}`);
            } else if (e.name === "TypeError") {
                throw new Error("fetchScriptContents: TypeError, method is not supported");
            } else {
                // A network error, or some other problem.
                throw e;
            }        
        });
}

This is my (multiple) attempt(s) at writing this test.
I defined a timeout of 500ms and mock the fetch implementation to resolve long after timeout (timeout + 1500ms).

if I don’t have an await on fetchScriptContents like so:

const resultPromise = fetchScriptContents("https://vzexample.com/script.js", timeout);

I would get a resolved value but no timeout will be hit.

but if I add an await before it
the test will always timeout. why is that? something wrong with my fetch mock? How do I properly write the test?


   beforeEach(() => {
        jest.clearAllMocks();
        jest.useFakeTimers();  // Use fake timers for controlling timeouts
    });

    afterEach(() => {
        jest.useRealTimers();
    });
    
    it('should throw', async () => {
        const timeout = 500;

        (global.fetch as jest.Mock) = jest.fn(() => new Promise((resolve, _reject) => setTimeout(() => resolve(mockResponse), timeout + 1500)));
        
        const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); 
        jest.spyOn(global, 'setTimeout');

        const resultPromise = fetchScriptContents("https://vzexample.com/script.js", timeout);
        jest.runAllTimers();
        expect(setTimeout).toHaveBeenCalledTimes(1);
        expect(abortSpy).toHaveBeenCalledTimes(1);

        await expect(fetchScriptContents("https://vzexample.com/script.js", timeout))
            .rejects
            .toThrow('Aborted by user action or fetch timeout');
});

I’ve read a lot about jest these recent days but I am out of ideas. Please help, thanks!