microsoft / playwright

Playwright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.
https://playwright.dev
Apache License 2.0
66.68k stars 3.65k forks source link

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

Open ruifigueira opened 2 months ago

ruifigueira commented 2 months ago

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.

Skn0tt commented 2 months ago

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 commented 2 months ago

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

ruifigueira commented 2 months ago

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.