typescriptpromiseplaywrightplaywright-typescript

Why can't I use page.locator in a page.waitForFunction?


I'm experimenting a bit with Playwright, with my Cypress background I'm comfortable with the cypress-wait-until function.

I try to wait until an expected number of a DOM elements exist. Meanwhile I found multiple solutions, but I wonder why my first approach did not work.

Maybe one of you has fallen also into the same trap?

Here is my test case.

  1. Open the web page

  2. Accept the cookies

  3. Search for Rembrandt van Rijn Pictures

  4. Wait until the WORKS OF ART image carousel has 10 items

test('search for rembrandt', async ({page, browser, context}) => {
    await page.goto('https://www.rijksmuseum.nl/en/content-search');
    // accept cookies
    await page.click('text=Accept');
    const inputSearch = page.locator("input.search-bar-input")
    await inputSearch.fill('Rembrandt van Rijn')
    const buttonSearch = page.locator("button[class$='search-bar-button']")
    await buttonSearch.click()
    // here comes the wait until code

})
  1. Try: ❌

     await page.waitForFunction(
         (page)=>
             page.locator('div[aria-label="Carousel with links to works of art"] div.carousel-item')
                 .count()
                 .then(nrOfElements => nrOfElements === 10), page)
    
  2. Try: ❌

     await page.waitForFunction(
         async (page)=>
            await page.locator('div[aria-label="Carousel with links to works of art"] div.carousel-item')
                 .count()
                 .then(nrOfElements => nrOfElements === 10), page)
    
    

    NOTE: You need to pass the page variable as args, otherwise you get: ReferenceError: page is not defined

    I always get: Error: Unexpected value enter image description here

    While the page.waitForFunction is able to handle Promises like this correctly:

    await page.waitForFunction(async() => await new Promise((resolve) => setTimeout(() => resolve(true), 10_000)))

    await page.waitForFunction(() => new Promise((resolve) => setTimeout(() => resolve(true), 10_000)))

  3. Quick and dirty but working:

    while(!await page.locator('div[aria-label="Carousel with links to works of art"] div.carousel-item').count().then(nrOfElements => nrOfElements ===10))

    NOTE: Here you will need an additional timeout, so the test stops if the number of elements does not match!

  4. The recommendation of the AI also working, but not so nice in my eyes:

    const extendArtQuerySelector = 'div[aria-label="Carousel with links to works of art"] div.carousel-item';
    
    // transferring only selector to browser context
    await page.waitForFunction(ellSelector => {
        const elements = document.querySelectorAll(ellSelector);
        return elements.length >= 10;
    }, extendArtQuerySelector);
    
  5. Finally the cleanest solution:

    await expect(page.locator('div[aria-label="Carousel with links to works of art"] div.carousel-item')).toHaveCount(10)

Has one of you an idea, why the page.locator does not work inside the waitForFunction?


Solution

  • Yes, waitForFunction's callback runs in the browser, which has no access to Playwright locators.

    Since you're writing a test, you can use expect(loc).toHaveLength(10):

    import {expect, test} from "@playwright/test"; // ^1.42.1
    
    test("search for rembrandt", async ({page}) => {
      await page.goto("<Your URL>");
      await page
        .getByRole("button", {name: "Accept", exact: true})
        .click();
      await page
        .getByPlaceholder("For info, artist, guided tour or more")
        .fill("Rembrandt van Rijn");
      await page
        .getByRole("button", {name: "Search", exact: true})
        .click();
      const carouselItems = page
        .getByLabel("Carousel with links to works of art")
        .locator(".carousel-item");
      await expect(carouselItems).toHaveCount(10);
    });
    

    Note I've attempted to use best practice user-visible locators rather than CSS where possible and avoided discouraged functions like page.click().

    If you want a "greater than or equal to" semantic, you can use:

    await expect(async () => {
      const count = await page
        .getByLabel("Carousel with links to works of art")
        .locator(".carousel-item")
        .count();
      expect(count).toBeGreaterThanOrEqual(10);
    }).toPass();
    

    If you're not using @playwright/test, then I think your document.querySelectorAll approach is acceptable, if a bit verbose. You can shorten it a little as follows:

    const playwright = require("playwright"); // ^1.42.1
    
    let browser;
    (async () => {
      browser = await playwright.firefox.launch();
      const page = await browser.newPage();
      await page.goto("<Your URL>");
      // ... same clicks/fills as above ...
      const sel =
        '[aria-label="Carousel with links to works of art"] .carousel-item';
      await page.waitForFunction(
        `[...document.querySelectorAll('${sel}')].length >= 10`
      );
    })()
      .catch(err => console.error(err))
      .finally(() => browser?.close());
    

    By the way, ChatGPT is pretty horrible at most anything related to Playwright--hallucinations out the wazoo. Proceed with extreme caution and test your assertions. it gave me some invalid code just now:

    // ❌ always passes even though it shouldn't
    await expect.toPass(() => {
      expect(42).toBeGreaterThanOrEqual(1000000);
    });
    

    This is an "always pass" assertion, even though it should fail. Very dangerous as it gives a false sense of confidence. While TS would catch this, the correct usage is:

    // ✅ fails as expected
    await expect(() => {
      expect(42).toBeGreaterThanOrEqual(1000000);
    }).toPass();