Search code examples
typescriptplaywrightplaywright-typescript

Individual timeout on image requests in playwright


I'm testing a web page using playwright. The website shows many externally hosted images that sometimes take forever to load, resulting in the tests to time out. All images are hosted on img.domain-name.com. In order to fix this issue, I'd like to abort long-running image requests.

The documentation shows a way to abort such requests via an interceptor like this:

await context.route(/^https://img.domain-name.com/, route => route.abort());

However, I'd like quick image requests to pass through and only abort those that are long-running.

I tried, setting a timeout for calling route.abort(), but realized I can only call abort OR continue, not both, as I tried.

await context.route(/^https://img.domain-name.com/, (route) => {
    const timer = setTimeout(() => {
        route.abort()
            .catch((error) => console.log(`[${url}] Aborting, caught error: ${error}`));
    }, 3_000);

    await route.continue()
        .then(() => clearTimeout(timer))
        .catch(() => clearTimeout(timer));
});

My second approach was to directly handle the requests and pass the responses. That seems to at least work, but results in many tests timing out and the images on the page not properly loading. In theory, the 3s timeout should result in almost all images loading correctly, with very few exceptions.

await context.route(/^https://img.domain-name.com/, (route) => {
    const timerPromise: Promise<Error> = new Promise((_, reject) =>
        setTimeout(() => reject(new Error('Request timed out')), 3_000)
    );

    try {
        const response: APIResponse | Error = await Promise.race([route.fetch(), timerPromise]);
        await route.fulfill({
            // @ts-ignore
            response
        });
    } catch (error) {
        console.log(`Error: ${error}`);
        await route.abort();
    }
});

Am I on the right track here or is there a more elegant solution?


Solution

  • Along the lines of the comment by @ggorlen, I eventually decided to change the objective from blocking long running image requests. Instead I'm now either blocking all image requests or none, if the test at hand requires image assets.

    To that end I created two test fixtures to use. One that blocks all image requests overriding the default page fixture and one that can explicitely be used if images are required:

    import { test as base } from '@playwright/test';
    
    export const test = base.extend({
        withImages: async ({ context, page }, use) => {
            await context.unroute(/^https://img.domain-name.com/);
            await use(page);
        },
    
        page: async ({ context, page }, use) => {
            await context.route(/^https://img.domain-name.com/, (route) => {
                route.abort();
            });
            await use(page);
        }
    });
    

    In tests I can now use either fixture:

    import { test } from './my-fixture';
    
    test('Test blocking image requests', async ({ page }) => {
        // Test code
    });
    
    test('Test with image assets', async ({ withImages }) => {
        const page = withImages;
        // Test code
    });