playwright icon indicating copy to clipboard operation
playwright copied to clipboard

[BUG] getByRole not finding implicit `generic` roles

Open WestonThayer opened this issue 1 year ago • 3 comments

System info

  • Playwright Version: [v1.40.1]
  • Operating System: macOS 14.1.2, but assume it shouldn't matter
  • Browser: Chromium
  • Other info:

Source code

Config file

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'], },
    },
  ]
});

Test file (self-contained)

it('should discover generic role elements', async ({ page }) => {
  await page.setContent(`
<div>One</div>
<div role="generic">Two</div>
  `);

  expect(await page.getByRole("generic").count()).toBe(2);

Steps

  • Run the test

Expected

The expectation passes, there are 2 "generic" role elements on the page.

Actual

The expectation fails because <div>One</div> is not discovered by getByRole("generic").

Via Chromium DevTools, note how the Accessibility tab shows the generic role for <div>One</div>:

Screenshot of DevTools with the One DOM element highlighted. DevTools Accessibility tab shows a role of generic.

WestonThayer avatar Dec 12 '23 20:12 WestonThayer

Noting that no role shown on https://playwright.dev/docs/api/class-locator#locator-get-by-role-option-role seems to discover <div>One</div>.

it('should discover generic role elements', async ({ page }) => {
  await page.setContent(`
<div>One</div>
<div role="generic">Two</div>
  `);

  const roles = [
    "alert",
    "alertdialog",
    "application",
    "article",
    "banner",
    "blockquote",
    "button",
    "caption",
    "cell",
    "checkbox",
    "code",
    "columnheader",
    "combobox",
    "complementary",
    "contentinfo",
    "definition",
    "deletion",
    "dialog",
    "directory",
    "document",
    "emphasis",
    "feed",
    "figure",
    "form",
    "generic",
    "grid",
    "gridcell",
    "group",
    "heading",
    "img",
    "insertion",
    "link",
    "list",
    "listbox",
    "listitem",
    "log",
    "main",
    "marquee",
    "math",
    "meter",
    "menu",
    "menubar",
    "menuitem",
    "menuitemcheckbox",
    "menuitemradio",
    "navigation",
    "none",
    "note",
    "option",
    "paragraph",
    "presentation",
    "progressbar",
    "radio",
    "radiogroup",
    "region",
    "row",
    "rowgroup",
    "rowheader",
    "scrollbar",
    "search",
    "searchbox",
    "separator",
    "slider",
    "spinbutton",
    "status",
    "strong",
    "subscript",
    "superscript",
    "switch",
    "tab",
    "table",
    "tablist",
    "tabpanel",
    "term",
    "textbox",
    "time",
    "timer",
    "toolbar",
    "tooltip",
    "tree",
    "treegrid",
    "treeitem",
  ];

  for (const role of roles) {
    const c = await page.getByRole(role).count();
    if (c > 0) {
      console.log(`${role}: ${c}`);
    }
  }

// Logs
// document: 1
// generic: 1

WestonThayer avatar Dec 12 '23 20:12 WestonThayer

Note that there are many more implicit generic elements than just <div>. https://www.w3.org/TR/html-aam/#html-element-role-mappings is probably the authoritative spec, but https://www.w3.org/TR/html-aria/#docconformance has the same info.

Confusingly, Chromium DevTools isn't always 1:1 with that table, for example <span> is shown with role "StaticText" even though it's exposed to the platform accessibility APIs in the same way as <div>.

WestonThayer avatar Jan 04 '24 04:01 WestonThayer

@mxschmitt Why does the test have to define the role? Can't that be exported so I don't need to define it.

diberry avatar Jan 23 '24 22:01 diberry

@WestonThayer What is your usecase for the generic role? Could you share a real test that makes use of it? Playwright uses null internally, and I would like to understand the usecase before making any fixes.

dgozman avatar Apr 17 '24 19:04 dgozman

Good question. Thanks for looking into this issue. I've learned a bunch since filing this issue that changes my perspective, and let me apologize in advance for not updating this issue, probably could've saved you some time thinking about it, but let me give you background first.

Say you were writing a test for GitHub's Actions page and wanted to make sure the workflow runs list has all the necessary info. Specifically, say you've seeded the data with a workflow run triggered by a PR and want to make sure the workflow name is present on the list item (highlighted below, "tests 1"):

Screenshot of https://github.com/microsoft/playwright/actions for a specific workflow run.

It's markup is essentially <span>tests 1</span>, not within a paragraph or other semantic container. You could write something like:

await expect(page.getByText("tests 1")).toBeVisible();

However, from an accessibility standpoint, that won't catch a regression that hides that text from the accessibility tree, like <span aria-hidden="true">tests 1</span>.

My attempt to address that was:

await expect(page.getByRole("generic", { name: "tests 1" })).toBeVisible();

I was led into thinking that was possible because getByRole("heading", { name: "Foo" }) works. So that was my motivation for filing this issue, wanting a way to validate that bits of static text are both present/visible and accessible, in the sense that they're available in the accessibility tree and thus can probably be reached by screen readers and other assistive tech.

That said, I've learned a lot about https://www.w3.org/TR/accname-1.1/ since filing this issue. I don't believe accessible names (accNames) are assigned to role=generic, which explains why Chrome DevTools doesn't show a name in my screenshot. https://www.w3.org/TR/wai-aria/#generic backs that up (aria-label is prohibited, not to mention the note about not using the role). https://www.w3.org/TR/wai-aria/#paragraph is similar.

So I'm not sure what the right solution is. Even if getByRole did find implicit generic roles, it's doesn't achieve my end goal because even getByRole("generic").hasText("tests 1") doesn't protect against that aria-hidden="true" case.

🤔 maybe getByText could have a flag that ensures the text is in the accessibility tree?

Let me know how you'd like to proceed. I can reframe this issue around the use case of "locate bits of static text that are not headings that are not hidden from the accessibility tree".

WestonThayer avatar Apr 17 '24 22:04 WestonThayer

@WestonThayer Thank you for the explanation. I'll think about this and come back.

dgozman avatar Apr 17 '24 23:04 dgozman