What is the reason behind the inability to utilize page.locator within a page.waitForFunction?

Delving into Playwright for the first time, coming from a Cypress background where I am familiar with the cypress-wait-until function.

I'm currently trying to wait until a specific number of DOM elements exist, and although I've found various solutions, I'm curious why my initial approach failed.

Perhaps someone else has encountered the same issue?

Below is the test case I am working on:

  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 displays 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()
    // waiting logic goes here

})
  1. Attempt: ❌

     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. Attempt: ❌

     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: Ensure that you pass the 'page' variable as args to avoid the 'ReferenceError: page is not defined'

    An error message stating: 'Error: Unexpected value' always appears. https://i.sstatic.net/FnYTWhVo.png

    The page.waitForFunction method can handle Promises correctly like this:

    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. A quick but effective solution:

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

    WARNING: This method requires an additional timeout to ensure the test stops if the element count doesn't match!

  4. The AI's recommendation also functions, but might not be as elegant:

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

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

Any thoughts on why the page.locator does not work within the waitForFunction?

Answer №1

Absolutely, when using waitForFunction, keep in mind that the callback will run within the browser environment and won't have access to Playwright locators.

For testing purposes, it's recommended to utilize 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 that I've opted for user-friendly locators over CSS wherever possible and avoided using functions like page.click() that are discouraged.

If you require a "greater than or equal to" check, consider this approach:

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 utilizing @playwright/test, your usage of document.querySelectorAll is acceptable but somewhat verbose. A streamlined version could look like this:

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 actions as described 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());

On a side note, ChatGPT may not be reliable when it comes to Playwright-related advice - it can sometimes produce incorrect code snippets. Ensure to validate your assertions properly. Here's an example of invalid code generated by ChatGPT:

// ❌ This always passes even though it should fail
await expect.toPass(() => {
  expect(42).toBeGreaterThanOrEqual(1000000);
});

This assertion always passes, which can lead to false confidence. To make sure tests work correctly, use the following pattern:

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

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Testing components in React involves creating or invoking specific construct or call signatures

Exploring this React component: import { Meta } from '@storybook/react'; export function MyComponentExample() { return ( <div>my component example</div> ); } export default { component: MyComponentExample, title: 'M ...

Exploring the Usage of Jasmine Testing for Subscribing to Observable Service in Angular's OnInit

Currently, I am facing challenges testing a component that contains a subscription within the ngOnInit method. While everything runs smoothly in the actual application environment, testing fails because the subscription object is not accessible. I have att ...

Tips for implementing asynchronous operations within the constructor

Exploring how to create a Customer object in NodeJs with the ability to retrieve its data. class CustomerModel extends Model { public customer constructor(email:string) { super(); this.collection = 'customer'; this.customer = aw ...

Is passing data through interfaces a suitable practice in TypeScript?

In my project, I have come across instances where an interface is being utilized instead of a class. For example: function check(car: ICar) { //perform some actions } this.check({mark: "Toyota", colour: "blue"}); Is it acceptable to continue using inter ...

Encountered an issue during the migration process from AngularJS to Angular: This particular constructor is not compatible with Angular's Dependency

For days, I've been struggling to figure out why my browser console is showing this error. Here's the full stack trace: Unhandled Promise rejection: NG0202: This constructor is not compatible with Angular Dependency Injection because its dependen ...

Start by prioritizing items with a higher click count when looping through the

I am creating a component to display recent search results. The API provides these results, including a value called "click_count". Each time a user clicks on a positive search result from the recent searches, this click_count value will increment by one. ...

Using TypeScript to return an empty promise with specified types

Here is my function signature: const getJobsForDate = async (selectedDate: string): Promise<Job[]> I retrieve the data from the database and return a promise. If the parameter selectedDate === "", I aim to return an empty Promise<Job[] ...

According to Typescript, the index property of each matchAll() match is of type (number | undefined)

Encountering an issue Having trouble with Type '(number | undefined)[]' is not assignable to type 'number[]'. whenever attempting to implement const nums: number[] = [...'a'.matchAll(/a/g)].map(match => match.index); desp ...

Creating a one-dimensional array without utilizing the FlatMap method

My objective is to access the 'values' array of the 'model' array separately. However, the 'flatMap' function is not available in my Angular application without adding "esnext" to the tsconfig.json file. I am exploring alterna ...

Creating applications with Angular2 and TypeScript is possible even without utilizing NPM for development

I've seen all the guides recommend installing npm, but I'm determined to find an alternative method. I found Angular2 files available here, however, none of them are in TypeScript code. What is the best course of action? Do I need Angular2.ts, ...

Null reference exception in Typescript + NextJS

I am facing an issue where the ref to a custom child component in my parent component is always null, preventing me from calling methods on it. Even though I believe I have implemented everything correctly, the ref (named CanvasUI) remains null and I can& ...

Error: AWS.CognitoIdentityCredentials does not work as a constructor

Encountering a puzzling issue with a custom-built AWS-SDK. Perhaps it's just a case of me missing the forest for the trees, but it's driving me crazy. Here's what's happening. I constructed an SDK incorporating all Cognito and DynamoDB ...

Delay the Ngrx effect by 2 seconds before initiating the redirect

I have an ngrx effect that involves calling an HTTP method and then waiting for 2 seconds before redirecting to another page. However, the current behavior is that it redirects immediately without waiting. confirmRegistration$ = createEffect(() => { ...

Having trouble troubleshooting React Typescript in Visual Studio Code?

I am currently facing a challenge while debugging a React Typescript application in VS Code. I am struggling to configure the launch.json file in order to enable TSX debugging. For bundling everything into a single JS file, I am utilizing webpack. Below ...

No routes found to match - Issue encountered in Ionic 5 Angular project

I have a total of 15 pages within my project and I am looking to incorporate a page with 2 tabs. To make this happen, I have created a folder labeled tabs inside the existing app directory. Inside the tabs folder, there are 3 specific pages - 1. project v ...

What is the best way to target the following input field in React Native for focus?

Is there a way to focus the next input field in React Native specifically on Android? It seems that the focus() function is only available in IOS. Any suggestions on how to achieve this? I am using React Native with TypeScript. https://i.sstatic.net/qzla ...

Changing Angular 2 web app code to Ionic 2 mobile app code?

I currently have a web application code that was written using Angular 2. My goal is to create a hybrid mobile application by utilizing Ionic 2 for the same web application. Since Ionic 2 incorporates core concepts of Angular 2, I have a few questions: Is ...

The parameter type '{ src: string; thumb: string; }' cannot be assigned to the 'never' type in the argument

Sample Typescript Code: I am experiencing issues with my code and cannot comprehend this error message- Argument of type '{ src: string; thumb: string; }' is not assignable to parameter of type 'never' _albums = []; constructor( ...

Retrieve user-specific information through a modal when the API is clicked

I am currently facing an issue where I am only able to retrieve the user ID or first name, but not all the details at once in the modal popup. My goal is to display specific user data in the modal. Here is the HTML code: <table class="table table- ...

What is the best way to change a Date stored in an array to a string format? [angular4]

Presented here is an array with the data labeled dateInterview:Date: public notes: Array<{ idAgreement: string, note: string, dateInterview: Date }> = []; My goal is to send this array to the server where all values of dateInterview need to be co ...