playwright
playwright copied to clipboard
[BUG] getByRole not finding implicit `generic` roles
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>:
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
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>.
@mxschmitt Why does the test have to define the role? Can't that be exported so I don't need to define it.
@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.
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"):
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 Thank you for the explanation. I'll think about this and come back.