How can I distinguish between element not visible and not rendered?

143 views Asked by At

I'm using Playwright for testing and need to differentiate between an element being hidden with CSS (via display, visibility, opacity, etc.) and an element not being rendered at all, meaning it's not part of the DOM.

In the Playwright documentation toBeHidden, it states: "Ensures that Locator either does not resolve to any DOM node, or resolves to a non-visible one."

const locator = page.locator('.my-element');
await expect(locator).toBeHidden();

From this test, I can't determine if .my-element is not rendered or simply hidden.

Is there a way in Playwright to distinguish between an element not being rendered and an element being hidden via CSS?

2

There are 2 answers

3
d.k On

Not sure if this is an optimal solution, but you can use:

await expect(loc).toBeAttached();
await expect(loc).toBeHidden();

if both of these pass — then it's hidden with CSS.

Also one could add a layer of abstraction, like:

await checkMyElementIsHiddenWithCss();

async function checkMyElementIsHiddenWithCss() {
  const locator = page.locator('.my-element');
  await Promise.all([
    expect(loc).toBeAttached(),
    expect(loc).toBeHidden(),
  ]);
}
0
ggorlen On

There seems to be some ambiguity about visibility in the answers posted so far. The question is actually pretty complex, and it's pretty easy to wind up with undesirable behavior.

Here's a runnable example to help figure out what's going on:

import {expect, test} from "@playwright/test"; // ^1.39.0

const html = `<!DOCTYPE html><html><body>
<p>normal</p>
<p style="visibility: hidden">hidden</p>
<p style="display: none">none</p>
</body></html>`;

test.beforeEach(({page}) => page.setContent(html));

test.describe("with two separate assertions: toBeHidden and toBeAttached", () => {
  test("an element with normal visibility fails", async ({page}) => {
    test.fail(); // this test should fail! the behavior is as expected.
    const loc = page.getByText("normal");
    await expect(loc).toBeHidden();
    await expect(loc).toBeAttached();
  });
  test("an element with visibility: hidden passes", async ({page}) => {
    const loc = page.getByText("hidden");
    await expect(loc).toBeHidden();
    await expect(loc).toBeAttached();
  });
  test("an element with display: none passes", async ({page}) => {
    const loc = page.getByText("none");
    await expect(loc).toBeHidden();
    await expect(loc).toBeAttached();
  });
  test("an element that does not exist fails", async ({page}) => {
    test.fail(); // this test should fail! the behavior is as expected.
    const loc = page.getByText("does not exist");
    await expect(loc).toBeHidden();
    await expect(loc).toBeAttached();
  });
});

test.describe("with one assertion: toBeVisible({visible: false})", () => {
  test("an element with normal visibility fails", async ({page}) => {
    test.fail(); // this test should fail! the behavior is as expected.
    const loc = page.getByText("normal");
    await expect(loc).toBeVisible({visible: false});
  });
  test("an element with visibility: hidden passes", async ({page}) => {
    const loc = page.getByText("hidden");
    await expect(loc).toBeVisible({visible: false});
  });
  test("an element with display: none passes", async ({page}) => {
    const loc = page.getByText("none");
    await expect(loc).toBeVisible({visible: false});
  });
  test("an element that does not exist passes (!)", async ({page}) => {
    // this passes but it's a false positive--the element doesn't exist
    const loc = page.getByText("does not exist");
    await expect(loc).toBeVisible({visible: false});
  });
});

test.describe("with one assertion: toBeHidden()", () => {
  test("an element with normal visibility fails", async ({page}) => {
    test.fail(); // this test should fail! the behavior is as expected.
    const loc = page.getByText("normal");
    await expect(loc).toBeHidden();
  });
  test("an element with visibility: hidden passes", async ({page}) => {
    const loc = page.getByText("hidden");
    await expect(loc).toBeHidden();
  });
  test("an element with display: none passes", async ({page}) => {
    const loc = page.getByText("none");
    await expect(loc).toBeHidden();
  });
  test("an element that does not exist passes (!)", async ({page}) => {
    // this passes but it's a false positive--the element doesn't exist
    const loc = page.getByText("does not exist");
    await expect(loc).toBeHidden();
  });
});

To summarize: only the dual assertion toBeHidden() and toBeAttached() captures both visible and invisible elements, with the caveat that it also considers display: none elements as attached. .toBeHidden() and toBeVisible({visible: false}) have the same behavior on the tests here.

It's also possible to bake the visibility test into the locator, which helps avoid an assertion:

test.describe("with :not(:visible) pseudoselector", () => {
  test("an element with normal visibility fails", async ({page}) => {
    test.fail(); // this test should fail! the behavior is as expected.
    const loc = page.locator(":text-is('normal'):not(:visible)");
    await expect(loc).toBeAttached();
  });
  test("an element with visibility: hidden passes", async ({page}) => {
    const loc = page.locator(":text-is('hidden'):not(:visible)");
    await expect(loc).toBeAttached();
  });
  test("an element with display: none passes", async ({page}) => {
    const loc = page.locator(":text-is('none'):not(:visible)");
    await expect(loc).toBeAttached();
  });
  test("an element that does not exist fails", async ({page}) => {
    test.fail(); // this test should fail! the behavior is as expected.
    const loc = page.locator(":text('does not exist'):not(:visible)");
    await expect(loc).toBeAttached();
  });
});

The behavior here is the same as the

await expect(loc).toBeHidden();
await expect(loc).toBeAttached();

version (at least on the tests here).

If you want to also assert that the element does not have display: none, the only way I can think of doing it at the moment is toHaveCSS. I'll update this if there's a better way:

test("an element with display: none fails", async ({page}) => {
  test.fail(); // this test should fail! the behavior is as expected.
  const loc = page.locator(":text-is('none'):not(:visible)");
  await expect(loc).toBeAttached();
  await expect(loc).not.toHaveCSS("display", "none");
});