Skip to content

feat(aria-snapshot): expose aria-invalid in ARIA snapshots#40966

Merged
pavelfeldman merged 2 commits into
microsoft:mainfrom
adityasingh2400:fix-34839
May 28, 2026
Merged

feat(aria-snapshot): expose aria-invalid in ARIA snapshots#40966
pavelfeldman merged 2 commits into
microsoft:mainfrom
adityasingh2400:fix-34839

Conversation

@adityasingh2400
Copy link
Copy Markdown
Contributor

Summary

  • Surface aria-invalid in ARIA snapshots so form-validation tests can assert on the invalid state. Adds an invalid boolean prop next to the existing checked / disabled / expanded / level / pressed shape, and emits [invalid] for elements whose role is in the WAI-ARIA 1.2 list (textbox, combobox, checkbox, radiogroup, spinbutton, etc.).
  • aria-invalid="true" | "grammar" | "spelling" all map to the boolean flag for snapshot purposes; the distinct values remain available through the existing getAriaInvalid helper for accessibility consumers.

Fixes #34839

Form-validation tests with ARIA snapshots could not assert on the
invalid state of an input, because `aria-invalid` was not surfaced
in the snapshot at all. Add an `invalid` boolean prop mirroring the
existing `checked` / `disabled` / `expanded` / `level` / `pressed`
shape, and emit `[invalid]` next to the role for elements whose role
appears in the WAI-ARIA 1.2 list of roles that support `aria-invalid`.

Treats `aria-invalid="true" | "grammar" | "spelling"` as `true` for
snapshot purposes; the distinct values are still available through
the existing `getAriaInvalid` helper for accessibility tree consumers.

Fixes: microsoft#34839
Copy link
Copy Markdown
Member

@Skn0tt Skn0tt left a comment

Choose a reason for hiding this comment

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

aria-invalid="true" | "grammar" | "spelling" all map to the boolean flag for snapshot purposes

I think we should expose the underlying range of values here, look at the pressed: true | false | mixed attribute for reference.

…hots

Mirror the pressed (true/false/mixed) precedent instead of collapsing
aria-invalid to a single boolean. The snapshot now surfaces the real
token: a true value renders as [invalid], grammar and spelling render
as [invalid=grammar] and [invalid=spelling], and a false value is
omitted. The template parser, matcher, types, docs, and tests are
updated to match.
@adityasingh2400
Copy link
Copy Markdown
Contributor Author

Good call. I now expose the underlying value instead of collapsing it to a boolean, mirroring the pressed (true/false/mixed) precedent. A true value renders as [invalid], grammar and spelling render as [invalid=grammar] and [invalid=spelling], and a false value is omitted (same as pressed=false). The template parser accepts true, false, grammar, and spelling, and any other non-false value still falls back to true per the ARIA spec. I updated the types, the matcher, the docs, and extended the test to cover the grammar and spelling tokens plus the false/omitted case. I ran the aria-snapshot suite locally on chromium and all 109 tests pass (to-match-aria-snapshot.spec.ts and page-aria-snapshot.spec.ts), including the existing pressed/checked/selected tests.

@github-actions
Copy link
Copy Markdown
Contributor

Test results for "tests others"

19 flaky ⚠️ [electron-page] › page/page-localstorage.spec.ts:49 › localStorage.removeItem removes a single item `@electron-ubuntu-latest`
⚠️ [electron-page] › page/page-navigation.spec.ts:28 › should work with cross-process _blank target `@electron-ubuntu-latest`
⚠️ [electron-page] › page/page-navigation.spec.ts:36 › should work with _blank target in form `@electron-ubuntu-latest`
⚠️ [electron-page] › page/page-request-continue.spec.ts:398 › should respect set-cookie in redirect response `@electron-ubuntu-latest`
⚠️ [electron-page] › page/page-route.spec.ts:74 › should not support ? in glob pattern `@electron-ubuntu-latest`
⚠️ [electron-page] › page/page-route.spec.ts:169 › should not override cookie header `@electron-ubuntu-latest`
⚠️ [electron-page] › page/page-localstorage.spec.ts:49 › localStorage.removeItem removes a single item `@electron-macos-latest`
⚠️ [electron-page] › page/page-navigation.spec.ts:28 › should work with cross-process _blank target `@electron-macos-latest`
⚠️ [electron-page] › page/page-navigation.spec.ts:36 › should work with _blank target in form `@electron-macos-latest`
⚠️ [electron-page] › page/page-request-continue.spec.ts:398 › should respect set-cookie in redirect response `@electron-macos-latest`
⚠️ [electron-page] › page/page-route.spec.ts:74 › should not support ? in glob pattern `@electron-macos-latest`
⚠️ [electron-page] › page/page-route.spec.ts:169 › should not override cookie header `@electron-macos-latest`
⚠️ [chromium-library] › library/inspector/recorder-api.spec.ts:120 › should type `@realtime-time-library-chromium-linux`
⚠️ [electron-page] › page/page-localstorage.spec.ts:49 › localStorage.removeItem removes a single item `@electron-windows-latest`
⚠️ [electron-page] › page/page-navigation.spec.ts:28 › should work with cross-process _blank target `@electron-windows-latest`
⚠️ [electron-page] › page/page-navigation.spec.ts:36 › should work with _blank target in form `@electron-windows-latest`
⚠️ [electron-page] › page/page-request-continue.spec.ts:398 › should respect set-cookie in redirect response `@electron-windows-latest`
⚠️ [electron-page] › page/page-route.spec.ts:74 › should not support ? in glob pattern `@electron-windows-latest`
⚠️ [electron-page] › page/page-route.spec.ts:169 › should not override cookie header `@electron-windows-latest`

19863 passed, 660 skipped


Merge workflow run.

@github-actions
Copy link
Copy Markdown
Contributor

Test results for "tests 2"

1 fatal errors, not part of any test
10 failed
❌ [installation tests] › playwright-electron-should-work.spec.ts:21 › electron should work @package-installations-ubuntu-latest-node26
❌ [installation tests] › playwright-electron-should-work.spec.ts:31 › electron should work with special characters in path @package-installations-ubuntu-latest-node26
❌ [installation tests] › playwright-electron-should-work.spec.ts:44 › should work when wrapped inside @playwright/test and trace is enabled @package-installations-ubuntu-latest-node26
❌ [firefox-library] › library/beforeunload.spec.ts:130 › should support dismissing the dialog multiple times @firefox-macos-26-xlarge
❌ [firefox-library] › library/screencast.spec.ts:28 › screencast.start delivers frames via onFrame callback @firefox-headed-macos-15-xlarge
❌ [firefox-library] › library/screencast.spec.ts:55 › onFrame receives viewport size @firefox-headed-macos-15-xlarge
❌ [firefox-library] › library/inspector/cli-codegen-2.spec.ts:142 › cli codegen › should upload multiple files @firefox-macos-26-large
❌ [firefox-library] › library/inspector/cli-codegen-2.spec.ts:172 › cli codegen › should clear files @firefox-macos-26-large
❌ [android-page] › page/page-evaluate.spec.ts:442 › should throw for too deep reference chain 2
❌ [android-page] › page/page-localstorage.spec.ts:99 › storage methods are scoped to the current origin

69 flaky ⚠️ [chromium-library] › library/browsertype-connect.spec.ts:189 › launchServer › should ignore page.pause when headed `@chromium-macos-26-large`
⚠️ [chromium-library] › library/browsertype-connect.spec.ts:431 › launchServer › should reject waitForEvent before browser.close finishes `@chromium-tip-of-tree-macos-15--headed`
⚠️ [chromium-library] › library/browsertype-connect.spec.ts:632 › launchServer › should properly disconnect when connection closes from the client side `@chromium-tip-of-tree-macos-15--headed`
⚠️ [chromium-library] › library/browsertype-connect.spec.ts:1117 › launchServer only › should be able to reconnect to a browser 12 times without warnings `@chromium-tip-of-tree-macos-15--headed`
⚠️ [chromium-library] › library/global-fetch.spec.ts:31 › post should work @smoke `@chromium-tip-of-tree-macos-15--headed`
⚠️ [chromium-library] › library/browsertype-connect.spec.ts:141 › launchServer › should be able to reconnect to a browser `@chromium-headed-macos-15-xlarge`
⚠️ [chromium-library] › library/browsertype-connect.spec.ts:431 › launchServer › should reject waitForEvent before browser.close finishes `@chromium-headed-macos-15-xlarge`
⚠️ [chromium-library] › library/browsertype-connect.spec.ts:189 › launchServer › should ignore page.pause when headed `@chromium-tip-of-tree-macos-15`
⚠️ [chromium-library] › library/video.spec.ts:719 › screencast › should work with video+trace `@chromium-tip-of-tree-macos-15`
⚠️ [chromium-library] › library/video.spec.ts:275 › screencast › should capture navigation `@chromium-ubuntu-24.04`
⚠️ [chromium-library] › library/video.spec.ts:337 › screencast › should work for popups `@channel-chromium-ubuntu-latest`
⚠️ [chromium-library] › library/trace-viewer.spec.ts:1196 › should show action source `@chromium-tip-of-tree-windows-latest--headed`
⚠️ [chromium-library] › library/global-fetch.spec.ts:246 › should return security details from response `@msedge-macos-latest`
⚠️ [chromium-library] › library/browsertype-connect.spec.ts:454 › launchServer › should respect selectors `@channel-chromium-macos-latest`
⚠️ [chromium-library] › library/browsertype-connect.spec.ts:686 › launchServer › should filter launch options `@channel-chromium-macos-latest`
⚠️ [chromium-page] › page/workers.spec.ts:63 › should have timestamp on worker console messages `@chromium-windows-latest`
⚠️ [chromium-library] › library/browsertype-connect.spec.ts:189 › launchServer › should ignore page.pause when headed `@chromium-macos-15-large`
⚠️ [chromium-page] › page/page-wait-for-selector-2.spec.ts:342 › should succeed if element handle was detached while waiting for hidden `@chromium-macos-15-large`
⚠️ [chromium-library] › library/browsertype-connect.spec.ts:189 › launchServer › should ignore page.pause when headed `@chromium-macos-14-xlarge`
⚠️ [chromium-library] › library/video.spec.ts:682 › screencast › should capture full viewport on hidpi `@chromium-macos-15-xlarge`
⚠️ [chromium-library] › library/proxy.spec.ts:93 › should proxy local network requests › by default › link-local `@chrome-beta-windows-latest`
⚠️ [chromium-library] › library/browsertype-connect.spec.ts:204 › launchServer › should be able to visit ipv6 through localhost `@msedge-beta-macos-latest`
⚠️ [chromium-library] › library/browsertype-connect.spec.ts:141 › launchServer › should be able to reconnect to a browser `@msedge-dev-macos-latest`
⚠️ [chromium-library] › library/chromium/connect-over-cdp.spec.ts:696 › should skip default overrides with noDefaults `@msedge-dev-macos-latest`
⚠️ [chromium-library] › library/browsertype-connect.spec.ts:141 › launchServer › should be able to reconnect to a browser `@chrome-beta-macos-latest`
⚠️ [chromium-library] › library/browsertype-connect.spec.ts:189 › launchServer › should ignore page.pause when headed `@chrome-beta-macos-latest`
⚠️ [chromium-library] › library/browsertype-connect.spec.ts:294 › launchServer › disconnected event should be emitted when browser is closed or server is closed `@chrome-beta-macos-latest`
⚠️ [chromium-library] › library/browsertype-connect.spec.ts:431 › launchServer › should reject waitForEvent before browser.close finishes `@chrome-beta-macos-latest`
⚠️ [chromium-library] › library/browsercontext-proxy.spec.ts:258 › should throw for socks4 authentication `@chrome-macos-latest`
⚠️ [installation tests] › connect-to-selenium.spec.ts:20 › connect to selenium `@package-installations-ubuntu-latest-node24`
⚠️ [installation tests] › playwright-cdn.spec.ts:75 › playwright cdn should not timeout on redirect `@package-installations-ubuntu-latest-node24`
⚠️ [installation tests] › playwright-electron-should-work.spec.ts:21 › electron should work `@package-installations-ubuntu-latest-node24`
⚠️ [installation tests] › playwright-electron-should-work.spec.ts:31 › electron should work with special characters in path `@package-installations-ubuntu-latest-node24`
⚠️ [installation tests] › playwright-packages-install-behavior.spec.ts:99 › @playwright/test should work `@package-installations-ubuntu-latest-node24`
⚠️ [firefox-library] › library/page-close.spec.ts:152 › should not treat navigations as new popups `@firefox-headed-macos-15-xlarge`
⚠️ [firefox-library] › library/browsercontext-basic.spec.ts:36 › should be able to click across browser contexts `@firefox-headed-ubuntu-24.04`
⚠️ [firefox-library] › library/browsercontext-basic.spec.ts:411 › should emulate media in cross-process iframe `@firefox-headed-ubuntu-24.04`
⚠️ [firefox-library] › library/inspector/cli-codegen-3.spec.ts:224 › cli codegen › should generate frame locators (4) `@firefox-beta-ubuntu-22.04`
⚠️ [firefox-page] › page/page-emulate-media.spec.ts:144 › should keep reduced motion and color emulation after reload `@tracing-firefox`
⚠️ [firefox-library] › library/client-certificates.spec.ts:885 › browser › persistentContext › should pass with matching certificates `@firefox-beta-windows-latest`
⚠️ [firefox-library] › library/browsercontext-storage-state.spec.ts:415 › should support IndexedDB `@firefox-headed-windows-latest`
⚠️ [firefox-library] › library/popup.spec.ts:132 › should use viewport size from window features `@firefox-headed-windows-latest`
⚠️ [firefox-library] › library/trace-viewer.spec.ts:210 › should keep selected action in view after Show all `@firefox-headed-windows-latest`
⚠️ [firefox-library] › library/trace-viewer.spec.ts:1816 › canvas disabled title `@firefox-headed-windows-latest`
⚠️ [firefox-library] › library/tracing.spec.ts:29 › should collect trace with resources, but no js `@firefox-headed-windows-latest`
⚠️ [firefox-library] › library/video.spec.ts:219 › screencast › should work with weird screen resolution `@firefox-headed-windows-latest`
⚠️ [firefox-page] › page/page-click-scroll.spec.ts:100 › should scroll into view element in iframe `@firefox-headed-windows-latest`
⚠️ [firefox-library] › library/video.spec.ts:275 › screencast › should capture navigation `@firefox-ubuntu-24.04`
⚠️ [firefox-library] › library/beforeunload.spec.ts:20 › should close browser with beforeunload page `@firefox-macos-26-large`
⚠️ [firefox-library] › library/defaultbrowsercontext-2.spec.ts:161 › should have passed URL when launching with ignoreDefaultArgs: true `@firefox-macos-26-large`
⚠️ [firefox-library] › library/defaultbrowsercontext-2.spec.ts:183 › should handle exception `@firefox-macos-26-large`
⚠️ [firefox-library] › library/browsercontext-basic.spec.ts:170 › should make a copy of default viewport `@firefox-beta-macos-latest`
⚠️ [firefox-page] › page/page-event-request.spec.ts:341 › should not expose preflight OPTIONS request with network interception `@firefox-beta-macos-latest`
⚠️ [firefox-library] › library/defaultbrowsercontext-2.spec.ts:161 › should have passed URL when launching with ignoreDefaultArgs: true `@firefox-macos-15-large`
⚠️ [firefox-library] › library/screenshot.spec.ts:280 › element screenshot › should restore default viewport after fullPage screenshot `@firefox-macos-15-large`
⚠️ [webkit-library] › library/video.spec.ts:371 › screencast › should scale frames down to the requested size `@webkit-macos-26-large`
⚠️ [webkit-library] › library/trace-viewer.spec.ts:480 › should highlight network request on timeline on hover `@webkit-headed-ubuntu-24.04`
⚠️ [webkit-page] › page/page-request-fallback.spec.ts:263 › post data › should amend json post data `@webkit-headed-macos-15-xlarge`
⚠️ [webkit-library] › library/proxy.spec.ts:290 › should bypass proxy for 127.0.0.1 when 127.0.0.1 is in bypass list `@tracing-webkit`
⚠️ [webkit-page] › page/page-click.spec.ts:1203 › should fire contextmenu event on right click in correct order `@tracing-webkit`
⚠️ [webkit-page] › page/page-set-input-files.spec.ts:38 › should upload a folder `@webkit-ubuntu-24.04`
⚠️ [webkit-library] › library/inspector/cli-codegen-csharp.spec.ts:208 › should print context options method override in nunit if options were passed `@webkit-macos-15-large`
⚠️ [webkit-library] › library/inspector/cli-codegen-javascript.spec.ts:84 › should save the codegen output to a file if specified `@webkit-macos-15-large`
⚠️ [webkit-library] › library/tracing.spec.ts:432 › should produce screencast frames fit `@webkit-macos-15-large`
⚠️ [webkit-library] › library/tracing.spec.ts:432 › should produce screencast frames scale `@webkit-macos-15-large`
⚠️ [android-page] › page/page-localstorage.spec.ts:49 › localStorage.removeItem removes a single item
⚠️ [android-page] › page/page-request-continue.spec.ts:162 › should override method along with url
⚠️ [android-page] › page/page-request-fallback.spec.ts:123 › should amend HTTP headers
⚠️ [android-page] › page/page-route.spec.ts:317 › should not throw if request was cancelled by the page

278251 passed, 11799 skipped


Merge workflow run.

@pavelfeldman pavelfeldman merged commit 2d04b34 into microsoft:main May 28, 2026
70 of 79 checks passed
2015kmadanap added a commit to Sapoto-Health/playwright-new that referenced this pull request May 28, 2026
…cer microsoft#1157 / Unit L) (#8)

Extends Playwright's ARIA snapshot output with two fields so an agent
can distinguish "menu collapsed" from "no popup at all" and reason
about MUI / Angular CDK overlay menus:

  haspopup       Rendered as bare `[haspopup]` for `aria-haspopup="menu"`
                 (the default value, per W3C ARIA 1.2 where `"true"` is
                 an alias for `"menu"`). Rendered as `[haspopup=<kind>]`
                 for `listbox`, `tree`, `grid`, `dialog`.

  expanded=false Explicitly rendered when `aria-expanded="false"`. The
                 previous default omitted false-state, only emitting
                 `[expanded]` for true — agents couldn't distinguish
                 a collapsed disclosure from a widget with no
                 expand/collapse affordance at all.

Match templates resolve correctly:
  - `[haspopup]` matches an element with `aria-haspopup="menu"`.
  - `[haspopup=listbox]` matches only `aria-haspopup="listbox"`.
  - `[haspopup=true]` is normalized to `[haspopup=menu]` at parse
    time, matching the runtime normalization in `getAriaHaspopup()`.
  - Negative case: `[haspopup=menu]` does NOT match
    `aria-haspopup="listbox"`.

Upstream's existing `[invalid]` rendering (added by
microsoft#40966) is preserved — `kAriaHaspopupRoles` is
gated by role, mirroring `kAriaInvalidRoles`. The `[invalid]` test
suite passes unchanged.

`<details>` elements (which map to `role="group"`) intentionally
remain outside the explicit `[expanded=false]` rendering — `group`
is not in `kAriaExpandedRoles`. The new behavior applies to widgets
that genuinely have an expand/collapse affordance (button, combobox,
treeitem, etc.).

Test plan:
  - `haspopup attribute renders kind` (3 cases — bare for menu,
    value-suffixed for non-default kinds).
  - `expanded=false rendering is explicit` (button with
    aria-expanded="false" renders [expanded=false]).
  - `haspopup match-template positive and negative`.
  - Existing `invalid attribute` test still passes (no regression).
  - Existing `expanded attribute` test still passes (no regression).

`npm run flint` clean. 39 / 39 in `to-match-aria-snapshot.spec.ts` pass.

Independent of other Sapoto units — can land any time per PRD's
implementation order.

Part of Sapoto-Health/automatic-document-fetcher#1150. Closes
Sapoto-Health/automatic-document-fetcher#1157.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Expose aria-invalid in ARIA Snapshots

3 participants