Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Question] Wait for image to be loaded #6046

Closed
Pooort opened this issue Apr 2, 2021 · 22 comments
Closed

[Question] Wait for image to be loaded #6046

Pooort opened this issue Apr 2, 2021 · 22 comments

Comments

@Pooort
Copy link

Pooort commented Apr 2, 2021

Hi.

I need to wait for a heavy image (6Mb) to be loaded.

I use:

await page.waitForResponse(src);

as suggested in #4782

But it doesn't work. Event fired at the start of loading. I see how it continue loading after this.

Thank you for help and for excellent product.

@pavelfeldman
Copy link
Member

You should do:

const response = await page.waitForResponse(src);
await response.finished();

@Pooort
Copy link
Author

Pooort commented Apr 4, 2021

Unfortunately it doesn't work either. It waits endlessly.

@pavelfeldman
Copy link
Member

Could you provide a snippet with the reproduction?

@Sapkotaanish
Copy link

waitUntil: 'networkidle'

This may work.

@Pooort
Copy link
Author

Pooort commented Apr 6, 2021

Could you provide a snippet with the reproduction?

const response = await page.waitForResponse(src); // I've got response
await response.finished(); // but this part continue endlessly
console.log('loaded'); // this one never happened

@Pooort
Copy link
Author

Pooort commented Apr 6, 2021

waitUntil: 'networkidle'

This may work.

Unfortunately it doesn't work either. It waits endlessly.

@Pooort
Copy link
Author

Pooort commented Apr 6, 2021

@pavelfeldman I'll try to make a test case with public image.

@dgozman
Copy link
Contributor

dgozman commented Apr 13, 2021

@Pooort Any luck with a public repro case?

@dgozman
Copy link
Contributor

dgozman commented Apr 19, 2021

Closing this one as we are unable to reproduce. Please comment here if you have a public repro case and we'll reopen.

@dgozman dgozman closed this as completed Apr 19, 2021
@dgozman dgozman removed the triaging label Apr 19, 2021
@luixo
Copy link

luixo commented May 3, 2023

That doesn't seem to be working, actually.

const image = ... // image locator
const imageSrc = await image.getAttribute("src");
if (imageSrc) {
  const imageResponse = await page.waitForResponse(imageSrc);
  await imageResponse.finished();
  const imagesTuples = await page.evaluate(async () => {
    const selectors = Array.from(document.querySelectorAll("img"));
    return selectors.map((img) => [img.src, img.complete]);
  });
  console.log("Images", imagesTuples);
}

As a result I get

Images [ [ 'https://picsum.photos/id/1/320/240', false ] ]

Do you need a reproduction of that?

@DoomyTheFroomy
Copy link

That doesn't seem to be working, actually.

const image = ... // image locator
const imageSrc = await image.getAttribute("src");
if (imageSrc) {
  const imageResponse = await page.waitForResponse(imageSrc);
  await imageResponse.finished();
  const imagesTuples = await page.evaluate(async () => {
    const selectors = Array.from(document.querySelectorAll("img"));
    return selectors.map((img) => [img.src, img.complete]);
  });
  console.log("Images", imagesTuples);
}

As a result I get

Images [ [ 'https://picsum.photos/id/1/320/240', false ] ]

Do you need a reproduction of that?

I think you will have a race condition here. To use waitForResponse you will have to declare this before opening the page, otherwise it may have been already loaded. You may have to (re)load the page again after you got all your image sources

@luixo
Copy link

luixo commented Jun 15, 2023

To use waitForResponse you will have to declare this before opening the page, otherwise it may have been already loaded.

I believe you assume whenever I get to my console.log the image is already loaded, right?
But why do I get img.complete as false in that case? Shouldn't it be true as image is already loaded?

@bonchuk
Copy link

bonchuk commented Oct 11, 2023

  await page.waitForFunction(() => {
    const images = Array.from(document.querySelectorAll('img'));
    return images.every(img => img.complete);
  });

This is works for me

@karlhorky
Copy link
Contributor

karlhorky commented Nov 6, 2023

Edit: We've had problems with waiting for all images to load using the await page.waitForLoadState('networkidle'), see the expect().toPass() alternative below


The version that we're currently using tests for naturalWidth:

// Wait for all network requests for images to complete
await page.waitForLoadState('networkidle');

// Scroll into view for lazy-loaded images
await page.locator('img').scrollIntoViewIfNeeded();

expect(
  await (
    await page.locator('img').evaluateHandle(
      (element, elementProperty) => {
        return element[elementProperty];
      },
      'naturalWidth',
    )
  ).jsonValue(),
).toBeGreaterThan(0);

Can be abstracted to a function to make it a bit nicer:

import { Locator } from '@playwright/test';

export async function getLocatorProperty<Element extends HTMLElement>(
  locator: Locator,
  property: string & keyof Element,
) {
  return (
    await locator.evaluateHandle(
      (element: Element, elementProperty: string) => {
        return element[elementProperty as keyof Element];
      },
      property,
    )
  ).jsonValue();
}

Usage:

// Wait for all network requests for images to complete
await page.waitForLoadState('networkidle');

// Scroll into view for lazy-loaded images
await page.locator('img').scrollIntoViewIfNeeded();

expect(
  await getLocatorProperty<HTMLImageElement>(
    page.locator('img'),
    'naturalWidth',
  ),
).toBeGreaterThan(0);

expect().toPass() alternative for waiting for an image to load

// Loop over the `naturalWidth` check until it passes,
// sometimes necessary for slow-loading images
await expect(async () => {
  expect(
    await (
      await page.locator('img').evaluateHandle(
        (element, elementProperty) => {
          return element[elementProperty];
        },
        'naturalWidth',
      )
    ).jsonValue(),
  ).toBeGreaterThan(0);
}).toPass();

@karlhorky
Copy link
Contributor

karlhorky commented Nov 6, 2023

@dgozman @pavelfeldman @mxschmitt what does the Playwright team think about paving the cowpaths on this one?

Eg. finding the best, robust recommended solution in the list above (including the part of waiting for network requests to finish) and then doing one or the other of the following:

  1. Creating a new first-party expect() assertion method like expect(page.locator('img')).toBeLoaded()
  2. Creating a recommended official recipe for this in the docs

To be clear, one statement of the value here is to test that programmatically-generated images are not broken in some way (which can easily happen silently, leaving them still "visible" and "in the viewport", but broken to website visitors)

If the Playwright team is open to this, I'm happy to create a new issue with full description and feature request or docs request.

@dgozman
Copy link
Contributor

dgozman commented Nov 7, 2023

@karlhorky This depends on what exactly you would like to achieve. Something like await expect(page.locator('img')).toBeLoaded() is possible, but you might as well have a custom matcher for this, since we have not seen much interest in such an assertion yet.

@karlhorky
Copy link
Contributor

karlhorky commented Nov 7, 2023

@dgozman interesting that there's not much uptake here. Maybe users don't know that they would want it?

Broken images can happen for a variety of reasons, including the one that I mentioned in the post above:

To be clear, one statement of the value here is to test that programmatically-generated images are not broken in some way (which can easily happen silently, leaving them still "visible" and "in the viewport", but broken to website visitors)

Pretty common to create programmatically-created image paths (or even programmatically-generated image files themselves) these days, especially with things like next/image.

Seems like a pretty big use case, and would be great if the Playwright team agrees that this common pattern is enough to justify a built-in matcher.

you might as well have a custom matcher for this

I'm not against having custom recipe-style code for this (instead of a built-in matcher) - this was my option 2 I posted above:

  1. Creating a recommended official recipe for this in the docs

But I would ask that the Playwright team comes up with their recommendation here with the fewest pitfalls and possibility for flaky tests. (already ran into some problems with my approach above since yesterday)

@dgozman
Copy link
Contributor

dgozman commented Nov 7, 2023

@karlhorky Unfortunately, I cannot speak to what is the best approach here, without testing it on a variety of different sites.

This solution seems to be very straightforward, but it waits instead of expecting. Perhaps the following expect will be good for your usecase?

await expect(page.locator('img')).toHaveJSProperty('complete', true);

If naturalWidth is a better indicator, then I'd structure my code like this:

await expect(page.locator('img')).not.toHaveJSProperty('naturalWidth', 0);

// or

await expect.poll(() => page.locator('img').evaluate(img => img.naturalWidth)).toBeGreaterThan(0);

Creating a recommended official recipe for this in the docs

Let's figure out the solution that works for most sites, and then we can definitely add it to the docs.

@karlhorky
Copy link
Contributor

karlhorky commented Nov 7, 2023

Great, those options look nice - I'll try complete and naturalWidth and see what the detection, performance and robustness characteristics look like across multiple test runs, with the following correct scenarios and breakage scenarios:

  1. Single image: image loads successfully
  2. Single image: image loads successfully (slow - large image on 3G network speed)
  3. Single image: image loads unsuccessfully (404)
  4. Single image: image loads unsuccessfully (malformed image)
  5. Single image: image loads unsuccessfully (host unreachable)
  6. Multiple images: all images load successfully
  7. Multiple images: all images load successfully (slow - large images on 3G network speed)
  8. Multiple images: all images load successfully (lazy loading out of viewport)
  9. Multiple images: 1st image loads unsuccessfully (404), rest load successfully
  10. Multiple images: all images but last load successfully, last image loads unsuccessfully (404)
  11. Multiple images: all images load unsuccessfully (404)
  12. Multiple images: all images load unsuccessfully (host unreachable)

@karlhorky
Copy link
Contributor

karlhorky commented Nov 8, 2023

Started a demo for all of these test cases here:

https://github.com/karlhorky/playwright-image-loading-tests-with-next-js

Now just need to find the time to actually hammer out the page HTML and test cases, maybe @ProchaLu will be able to drop in for some help here... 🤔

@karlhorky
Copy link
Contributor

karlhorky commented Nov 8, 2023

Hm, also just thinking that if .toHaveJSProperty() works for this case, it could also be used to detect video loading using readyState === 4:

// readyState 4 means "Enough data is available to play video"
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState
const video = page.locator('video').first();
await video.scrollIntoViewIfNeeded();
await expect(video).toHaveJSProperty('readyState', 4);

Edit: Seems to work 👍

@ProchaLu
Copy link

ProchaLu commented Nov 9, 2023

@dgozman @karlhorky
The demo for all of these test cases here:
https://github.com/karlhorky/playwright-image-loading-tests-with-next-js

We recognized in these test scenarios that the img.complete property is not a reliable indicator of whether an image has loaded successfully. It is only a reliable indicator of whether the image has loaded at all. A broken image will still report img.complete = true. Therefore, the more reliable indicator of whether an image has loaded successfully is the img.naturalWidth property. A broken image will report img.naturalWidth = 0.

Here are the test results of the different scenarios:

1. Single image: image loads successfully

<Image src="next.svg" alt="image" width="{300}" height="{300}" />

The following is the console output from the test run for img.complete and img.naturalWidth:

c: complete with true or false
nW: naturalWidth with 0 or 1+ (1+ means > 0)

c: f nW: 0
c: t nW: 1+
c: t nW: 1+
...

Playwright test is successful.

await page.goto('/01-single-success');
await expect(page.locator('img')).toHaveJSProperty('complete', true);
await expect(page.locator('img')).not.toHaveJSProperty('naturalWidth', 0);

2. Single image: image loads successfully (slow - large image on 3G network speed)

<Image src="next.svg" alt="image" width="{300}" height="{300}" />
c: f nW: 0
c: t nW: 1+
c: t nW: 1+
...

Playwright test is successful. For this test the network speed was throttled to 3G.

const client = await page.context().newCDPSession(page);

await client.send('Network.enable');

await client.send('Network.emulateNetworkConditions', {
  offline: false,
  downloadThroughput: ((500 * 1000) / 8) * 0.8,
  uploadThroughput: ((500 * 1000) / 8) * 0.8,
  latency: 70,
});
await page.goto('/02-single-success-slow');
await expect(page.locator('img')).toHaveJSProperty('complete', true);
await expect(page.locator('img')).not.toHaveJSProperty('naturalWidth', 0);

3. Single image: image loads unsuccessfully (404)

<Image src="404.svg" alt="image" width={300} height={300} />
c: f nW: 0
c: t nW: 0
c: t nW: 0
...

The Playwright test below for for img.naturalWidth fails, because the is broken (404) but reports img.complete = true.

Failed to load resource: the server responded with a status of 404 (Not Found)
await page.goto('/03-single-fail-404');
await expect(page.locator('img')).toHaveJSProperty('complete', true);
await expect(page.locator('img')).not.toHaveJSProperty('naturalWidth', 0);

4. Single image: image loads unsuccessfully (malformed image)

<Image src="/malformed.svg" alt="image" width={300} height={300} />
c: f nW: 0
c: t nW: 0
c: t nW: 0
...

The Playwright test below for for img.naturalWidth fails, because the is broken (malformed) but reports img.complete = true.

Failed to load resource: the server responded with a status of 400 (Bad Request)
await page.goto('/04-single-fail-malformed');
await expect(page.locator('img')).toHaveJSProperty('complete', true);
await expect(page.locator('img')).not.toHaveJSProperty('naturalWidth', 0);

5. Single image: image loads unsuccessfully (host unreachable)

<Image src="https://i/img.jpg" alt="image" width={300} height={300} />
c: f nW: 0
c: t nW: 0
c: t nW: 0
...

The Playwright test below for for img.naturalWidth fails, because the is broken (host unreachable) but reports img.complete = true.

Failed to load resource: the server responded with a status of 500 (Internal Server Error)
await page.goto('/05-single-fail-unreachable');
await expect(page.locator('img')).toHaveJSProperty('complete', true);
await expect(page.locator('img')).not.toHaveJSProperty('naturalWidth', 0);

6. Multiple images: all images load successfully

  <Image src="next.svg" alt="image" width={200} height={200} />
  <br />
  <Image src="next.svg" alt="image" width={200} height={200} />
  <br />
  <Image src="next.svg" alt="image" width={200} height={200} />
  <br />
  <Image src="next.svg" alt="image" width={200} height={200} />
  <br />
  <Image src="next.svg" alt="image" width={200} height={200} />
  <br />
  <Image src="next.svg" alt="image" width={200} height={200} />
  <br />
  <Image src="next.svg" alt="image" width={200} height={200} />
  <br />
c: f f f f f f f nW: 0 0 0 0 0 0 0
c: t t t t t t t nW: 1+ 1+ 1+ 1+ 1+ 1+ 1+
c: t t t t t t t nW: 1+ 1+ 1+ 1+ 1+ 1+ 1+
...

Playwright test is successful.

await page.goto('/06-multiple-success');
for (const img of await page.getByRole('img').all()) {
  await expect(img).toHaveJSProperty('complete', true);
  await expect(img).not.toHaveJSProperty('naturalWidth', 0);
}

7. Multiple images: all images load successfully (slow - large images on 3G network speed)

  <Image src="/large.png" alt="image" width={1800} height={1000} />
  <br />
  <Image src="/large.png" alt="image" width={1800} height={1000} />
  <br />
  <Image src="/large.png" alt="image" width={1800} height={1000} />
  <br />
  <Image src="/large.png" alt="image" width={1800} height={1000} />
  <br />
  <Image src="/large.png" alt="image" width={1800} height={1000} />
  <br />
  <Image src="/large.png" alt="image" width={1800} height={1000} />
  <br />
  <Image src="/large.png" alt="image" width={1800} height={1000} />
c: f f f f f f f nW: 0 0 0 0 0 0 0
c: t t t f f f f nW: 1+ 1+ 1+ 0 0 0 0
c: t t t t f f f nW: 1+ 1+ 1+ 1+ 0 0 0
c: t t t t t f f nW: 1+ 1+ 1+ 1+ 1+ 0 0
c: t t t t t t f nW: 1+ 1+ 1+ 1+ 1+ 1+ 0
c: t t t t t t t nW: 1+ 1+ 1+ 1+ 1+ 1+ 1+
c: t t t t t t t nW: 1+ 1+ 1+ 1+ 1+ 1+ 1+
...

Playwright test is successful. For this test the network speed was throttled to 3G.

const client = await page.context().newCDPSession(page);

await client.send('Network.enable');

await client.send('Network.emulateNetworkConditions', {
  offline: false,
  downloadThroughput: ((500 * 1000) / 8) * 0.8,
  uploadThroughput: ((500 * 1000) / 8) * 0.8,
  latency: 70,
});
await page.goto('/07-multiple-success-slow');
for (const img of await page.getByRole('img').all()) {
  await expect(img).toHaveJSProperty('complete', true);
  await expect(img).not.toHaveJSProperty('naturalWidth', 0);
}

8. Multiple images: all images load successfully (lazy loading out of viewport)

...
  <Image
    src="next.svg"
    alt="image"
    width={200}
    height={200}
    loading="lazy"
  />
  <br />
  <br />
  <br />
  <br />
  <br />
  <br />
  <br />
  <br />
  <br />
  <br />
  <br />
  <br />
  <Image
    src="next.svg"
    alt="image"
    width={200}
    height={200}
    loading="lazy"
  />
...
c: f f f f f f f nW: 0 0 0 0 0 0 0
c: t t t t t t t nW: 1+ 1+ 1+ 1+ 1+ 1+ 1+
c: t t t t t t t nW: 1+ 1+ 1+ 1+ 1+ 1+ 1+
...

Playwright test is successful.

await page.goto('/08-multiple-success-lazy');
for (const img of await page.getByRole('img').all()) {
  await expect(img).toHaveJSProperty('complete', true);
  await expect(img).not.toHaveJSProperty('naturalWidth', 0);
}

9. Multiple images: 1st image loads unsuccessfully (404), rest load successfully

  <Image src="404.svg" alt="image" width={300} height={300} />
  <br />
  <Image src="next.svg" alt="image" width={300} height={300} />
  <br />
  <Image src="next.svg" alt="image" width={300} height={300} />
  <br />
  <Image src="next.svg" alt="image" width={300} height={300} />
  <br />
  <Image src="next.svg" alt="image" width={300} height={300} />
  <br />
  <Image src="next.svg" alt="image" width={300} height={300} />
  <br />
  <Image src="next.svg" alt="image" width={300} height={300} />
c: f f f f f f f nW: 0 0 0 0 0 0 0
c: f t t t t t t nW: 0 1+ 1+ 1+ 1+ 1+ 1+
c: t t t t t t t nW: 0 1+ 1+ 1+ 1+ 1+ 1+
c: t t t t t t t nW: 0 1+ 1+ 1+ 1+ 1+ 1+
...

The Playwright test below for img.loaded fails, because the first image is broken (404) but reports img.complete = 'true'

Failed to load resource: the server responded with a status of 404 (Not Found)
await page.goto('/09-multiple-1st-fail-rest-success');
for (const img of await page.getByRole('img').all()) {
  await expect(img).toHaveJSProperty('complete', true);
  await expect(img).not.toHaveJSProperty('naturalWidth', 0);
}

10. Multiple images: all images but last load successfully, last image loads unsuccessfully (404)

  <Image src="next.svg" alt="image" width={300} height={300} />
  <br />
  <Image src="next.svg" alt="image" width={300} height={300} />
  <br />
  <Image src="next.svg" alt="image" width={300} height={300} />
  <br />
  <Image src="next.svg" alt="image" width={300} height={300} />
  <br />
  <Image src="next.svg" alt="image" width={300} height={300} />
  <br />
  <Image src="next.svg" alt="image" width={300} height={300} />
  <br />
  <Image src="404.svg" alt="image" width={300} height={300} />
c: f f f f f f f nW: 0 0 0 0 0 0 0
c: t t t t t t f nW: 1+ 1+ 1+ 1+ 1+ 1+ 0
c: t t t t t t t nW: 1+ 1+ 1+ 1+ 1+ 1+ 0
c: t t t t t t t nW: 1+ 1+ 1+ 1+ 1+ 1+ 0
...

The Playwright test below for img.loaded fails, because the last image is broken (404) but reports img.complete = 'true'

Failed to load resource: the server responded with a status of 404 (Not Found)
await page.goto('/10-multiple-last-fail-rest-success');
for (const img of await page.getByRole('img').all()) {
  await expect(img).toHaveJSProperty('complete', true);
  await expect(img).not.toHaveJSProperty('naturalWidth', 0);
}

11. Multiple images: all images load unsuccessfully (404)

  <Image src="404.svg" alt="image" width={300} height={300} />
  <br />
  <Image src="404.svg" alt="image" width={300} height={300} />
  <br />
  <Image src="404.svg" alt="image" width={300} height={300} />
  <br />
  <Image src="404.svg" alt="image" width={300} height={300} />
  <br />
  <Image src="404.svg" alt="image" width={300} height={300} />
  <br />
  <Image src="404.svg" alt="image" width={300} height={300} />
  <br />
  <Image src="404.svg" alt="image" width={300} height={300} />
c: f f f f f f f nW: 0 0 0 0 0 0 0
c: t t t t t t t nW: 0 0 0 0 0 0 0
c: t t t t t t t nW: 0 0 0 0 0 0 0
...

The Playwright test below for img.loaded fails, because all images are broken (404) but reports img.complete = 'true'

Failed to load resource: the server responded with a status of 404 (Not Found)
await page.goto('/11-multiple-fail-404');
for (const img of await page.getByRole('img').all()) {
  await expect(img).toHaveJSProperty('complete', true);
  await expect(img).not.toHaveJSProperty('naturalWidth', 0);
}

12. Multiple images: all images load unsuccessfully (host unreachable)

<div>
  <Image src="https://i/img.jpg" alt="image" width={300} height={300} />
  <br />
  <Image src="https://i/img.jpg" alt="image" width={300} height={300} />
  <br />
  <Image src="https://i/img.jpg" alt="image" width={300} height={300} />
  <br />
  <Image src="https://i/img.jpg" alt="image" width={300} height={300} />
  <br />
  <Image src="https://i/img.jpg" alt="image" width={300} height={300} />
  <br />
  <Image src="https://i/img.jpg" alt="image" width={300} height={300} />
  <br />
  <Image src="https://i/img.jpg" alt="image" width={300} height={300} />
</div>
c: f f f f f f f nW: 0 0 0 0 0 0 0
c: t t t t t t t nW: 0 0 0 0 0 0 0
c: t t t t t t t nW: 0 0 0 0 0 0 0
...

The Playwright test below for img.loaded fails, because all images are broken (host unreachable) but reports img.complete = 'true'

Failed to load resource: the server responded with a status of 500 (Internal Server Error)
await page.goto('/12-multiple-fail-unreachable');
for (const img of await page.getByRole('img').all()) {
  await expect(img).toHaveJSProperty('complete', true);
  await expect(img).not.toHaveJSProperty('naturalWidth', 0);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants