playwright icon indicating copy to clipboard operation
playwright copied to clipboard

[Docs]: add use case for closing multiple elements like toast notifications

Open ruifigueira opened this issue 1 year ago • 3 comments
trafficstars

Page(s)

https://playwright.dev/docs/api/class-page#page-add-locator-handler

Description

I'm trying to close multiple toast notifications with a addLocatorHandler, since they are asynchronous and I cannot predict when they are displayed.

I tried this approach but it fails because page.locator('div') is not strict and returns multiple notiifications:

test('test 1', async ({ page }) => {
  await page.addLocatorHandler(page.locator('div'), async notifications => {
    while (await notifications.count() > 0)
      await notifications.first().click();
  });

  await page.evaluate(() => {
    for (let i = 1; i <= 4; i++) {
      const elem = document.createElement('div');
      elem.textContent = `Notification #${i}`;
      elem.addEventListener('click', () => elem.remove());
      document.body.appendChild(elem);
    }
  });
  await expect(page.locator('div')).toHaveCount(0);
});

I cannot close one by one because after the first one is handled it doesn't retry again before the assertion check:

test('test 2', async ({ page }) => {
  await page.addLocatorHandler(page.locator('div').first(), locator => locator.click());

  await page.evaluate(() => {
    for (let i = 1; i <= 4; i++) {
      const elem = document.createElement('div');
      elem.textContent = `Notification #${i}`;
      elem.addEventListener('click', () => elem.remove());
      document.body.appendChild(elem);
    }
  });
  await expect(page.locator('div')).toHaveCount(0);
});

The only way I found is to pass a parent locator with a filter to ensure it has notifications inside:

test('test 3', async ({ page }) => {
  await page.addLocatorHandler(page.locator('body', { has: page.locator('div') }), async () => {
    const notifications = page.locator('div');
    while (await notifications.count() > 0)
      await notifications.first().click();
  });

  await page.evaluate(() => {
    for (let i = 1; i <= 4; i++) {
      const elem = document.createElement('div');
      elem.textContent = `Notification #${i}`;
      elem.addEventListener('click', () => elem.remove());
      document.body.appendChild(elem);
    }
  });
  await expect(page.locator('div')).toHaveCount(0);
});

This last solution is acceptable but it's not obvious, so maybe these kind of scenarios could be documented.

Also, it would be even better if the locator could select more than one element, and then it would be up to the handler function to handle it properly, similar to test 1 above. If you think that's worth it, I can even pick that task and try to implement it.

ruifigueira avatar Aug 20 '24 23:08 ruifigueira

Hey Rui! This is the first time we're hearing about this usecase, so i'm a little wary of adding it to the docs straight away. The solution you provide looks sound. Let's keep it in this issue, and see if others stumble upon. If there's lots of folks interested in it, then we can put it in the docs.

Skn0tt avatar Aug 22 '24 10:08 Skn0tt

Also, on your third snippet, have you tried page.locator('div').first()? That should also work fine.

Skn0tt avatar Aug 22 '24 10:08 Skn0tt

Yes, you're right. In my test 2 scenario I was just closing that element and I expected the handler to be called for each other element, but it's not the way it works, as explained in the documentation.

ruifigueira avatar Aug 22 '24 14:08 ruifigueira

Why was this issue closed?

Thank you for your involvement. This issue was closed due to limited engagement (upvotes/activity), lack of recent activity, and insufficient actionability. To maintain a manageable database, we prioritize issues based on these factors.

If you disagree with this closure, please open a new issue and reference this one. More support or clarity on its necessity may prompt a review. Your understanding and cooperation are appreciated.

pavelfeldman avatar Sep 04 '25 01:09 pavelfeldman