Skip to content

[HEIHEI-928] fix: infer columnheader/rowheader for th without scope - #25

Open
bbrzoska wants to merge 1 commit into
vitest-dev:masterfrom
bbrzoska:fix/th-implicit-role
Open

bbrzoska wants to merge 1 commit into
vitest-dev:masterfrom
bbrzoska:fix/th-implicit-role

Conversation

@bbrzoska

@bbrzoska bbrzoska commented Sep 30, 2026 •

Copy link
Copy Markdown

Refs vitest-dev/vitest#8054

Problem

getByRole('columnheader') and getByRole('rowheader') in Vitest browser mode don't find a <th> that has no scope attribute. ivya treats such a th as a cell, which is how Playwright behaved when #8054 was closed. Playwright has since changed the rule in microsoft/playwright#38160 (released in 1.57), so for the same markup Playwright's page.getByRole('columnheader') now finds the header cells and Vitest's doesn't.

Change

This ports Playwright's current TH rule from packages/injected/src/roleUtils.ts into src/roleUtils.ts, including the two helpers isHeaderCell and isNonEmptyDataCell:

  • An explicit scope still wins: col/colgroup gives columnheader, and row/rowgroup gives rowheader. colgroup and rowgroup weren't recognised before.
  • Otherwise the role comes from the neighbouring cells, as Chromium does it (ax_node_object.cc). A th with a non-empty td next to it is a rowheader. Any other th is a columnheader. The one exception is a lone th in a single-row table, which gets no role.

Tests

test/role.test.ts runs on chromium, firefox and webkit. It covers these cases, taken from the ones Playwright added in #38160:

  • a header-row th is a columnheader, with or without thead
  • a th first in a body row is a rowheader
  • a th next to an empty td is a columnheader
  • a lone th in a single-row table has no role, and in a multi-row table it's a columnheader
  • all four scope values beat the position-based rule
  • the table from #8054 works end to end through queryAllByRole and getByRoleSelector

Nine of these fail without the change. The table structure roles aria snapshot now shows columnheader "H" where it used to show cell "H", which is what Playwright's snapshot shows.

I also checked every fixture outside the test suite with Playwright 1.60 on Chromium. For each one I compared ivya's new result with Playwright's page.getByRole and with Chromium's accessibility tree from CDP Accessibility.getPartialAXTree. They matched in all eight cases, including the lone th (Playwright gives no role, and Chromium reports a layout-only LayoutTableCell).

Once this is released, Vitest needs its ivya dependency bumped to pick it up.

🤖 Generated with Claude Code

Port Playwright's implicit role rule for `th` (microsoft/playwright#38160),
which matches Chromium: an explicit `scope` of col/colgroup or row/rowgroup
wins; otherwise the role comes from the neighbouring cells, and a lone `th`
in a single-row table gets no role. Previously a `th` without `scope` was
treated as a `cell`, so `getByRole('columnheader')` missed header cells that
the browser's accessibility tree and Playwright both expose as headers.

Refs vitest-dev/vitest#8054

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings September 30, 2026 22:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@bbrzoska bbrzoska changed the title fix: infer columnheader/rowheader for th without scope [HEIHEI-928] fix: infer columnheader/rowheader for th without scope Oct 1, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants