Search code examples
playwright

Is there any way to select (i.e. create locators) by style value?


In Playwright there are a lot of ways to create locators, mostly involving CSS selectors. However, as far as I can tell, there's no way to say "find the red button" (i.e. the <button> with background-color: red).

If the button has a red class, then of course you can select .red, but what if you don't have a class? If you only care about certain styles (e.g. display) you can use special Playwright selectors (e.g. :visible), but they only work for those specific styles.

Is there any general purpose way to find elements based on any style value?


Solution

  • There is no way I know of to do this out of the box, and I'm not sure that it's a good pattern.

    Technically, something like this works:

    import {test} from "@playwright/test"; // ^1.42.1
    
    const html = `<!DOCTYPE html>
    <html>
    <body>
    <button>Yellow</button>
    <button>Blue</button>
    <button>Red</button>
    <script>
    setTimeout(() => {
      document.querySelector("button:last-of-type").style.color = "red";
    }, 3000);
    </script>
    </body>
    </html>`;
    
    test("red button eventually exists", async ({page}) => {
      await page.setContent(html);
      const redButtonExists = `
        [...document.querySelectorAll("button")]
          .some(e => getComputedStyle(e).color === "rgb(255, 0, 0)")
      `;
      await page.waitForFunction(redButtonExists);
    });
    

    Problems:

    1. This circumvents the standard selection best practices and functions provided by Playwright (and any other browser automation library I know of).

    2. It's an expensive polling loop and getComputedStyle call.

    3. "red" was converted by the browser to "rgb(255, 0, 0)", not something that's obvious (or necessarily guaranteed?). I haven't tested the behavior in other browsers, but it smells iffy.

    4. Assertions like this aren't used often, so it's best to follow suit with standard selection practices.

      Aria role is more stable and better practice. Test IDs would also be better, or selecting by text, value or attribute, or even a CSS attribute which is objective and doesn't pass through cascading styles and browser-specific interpretation.

    5. Styles are generally pretty subjective and dynamic, so something like "find the red button" sounds logical to a human, but doesn't make a lot of sense in browser automation since there's no obvious definition of what "redness" entails, precisely, beyond the RGB definition.

      An algorithm could give you a "close enough" reading--maybe that's what you're after. But that's just for color--the same problem exists for just about every other style. How rounded do rounded corners have to be to qualify? This seems like a deep pit of complexity and ambiguity with no obvious gain over established practices.

    6. I can't imagine a test suite filled with "whack the red button, then hit the green one" would be anything but a headache to understand and maintain. Most sites with colored buttons have multiple buttons on the page with the same color, so to disambiguate between those, you're forced back in the standard role/name, text, test id, value, and attribute selection paradigm you tried to avoid.

      First/last/nth are not considered good practice, because order is pretty arbitrary, so this isn't a way out of the problem.

    In the above example, the best way to select the Red button is

    const redButton = page.getByRole("button", {name: "Red", exact: true});
    await expect(redButton).toBeVisible();
    

    Consider that if the text changes and the test breaks, that's actually good. The goal is not to have a test suite that never breaks, but one that can be easily maintained to be in lockstep with the rendered reality and with the application's specification. (And text content is generally a critical part of a specification, arguably the most important thing on the page. A test suite that prefers enforcing colors and styles over the actual page content seems misguided. Think accessibility: what is the user experience for a blind person? Test that.)

    You may be interested in screenshot tests, which implicitly assert colors of all elements within the screenshot area, but this assertion style has its own set of drawbacks and isn't a replacement for specific assertions on role, text and so forth.

    If you only care about styles in style= attributes, see this thread. This has a clear use case: web scraping when there is no other way to distinguish a particular element. Tests should never touch style= attributes (and codebases you control shouldn't be using them in the first place).

    Finally, there is a toHaveCSS assertion, but it doesn't quite work for your use case:

    test("red button eventually exists", async ({page}) => {
      await page.setContent(html);
      await expect(page.getByRole("button")).toHaveCSS("color", "rgb(255, 0, 0)");
    });
    

    This fails with:

    Error: expect.toHaveCSS: Error: strict mode violation: getByRole('button') resolved to 3 elements:
        1) <button>Yellow</button> aka getByRole('button', { name: 'Yellow' })
        2) <button>Blue</button> aka getByRole('button', { name: 'Blue' })
        3) <button>Red</button> aka getByRole('button', { name: 'Red' })
    

    This assertion may be appropriate if you already have the element located and there's a good reason to test a particular CSS property.