From 31d17972e6d47afd4ee5d87cd573199fdda95dcf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 May 2025 23:50:56 +0000 Subject: [PATCH 01/13] Initial plan for issue From c9453ede0ce8636cfd47fadb37f9ebaa6f9c795f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 May 2025 00:16:46 +0000 Subject: [PATCH 02/13] Add focused and focusable properties to ariaSnapshot for _snapshotForAI Co-authored-by: yury-s <9798949+yury-s@users.noreply.github.com> --- packages/injected/src/ariaSnapshot.ts | 10 +++++++- packages/injected/src/roleUtils.ts | 2 +- .../src/server/chromium/crAccessibility.ts | 3 +++ .../src/server/firefox/ffAccessibility.ts | 4 ++++ .../src/server/webkit/wkAccessibility.ts | 3 +++ .../src/utils/isomorphic/ariaSnapshot.ts | 12 ++++++++++ tests/page/page-aria-snapshot-ai.spec.ts | 24 +++++++++++++++++++ 7 files changed, 56 insertions(+), 2 deletions(-) diff --git a/packages/injected/src/ariaSnapshot.ts b/packages/injected/src/ariaSnapshot.ts index a75a5cd185d6c..0f03f252f8cb8 100644 --- a/packages/injected/src/ariaSnapshot.ts +++ b/packages/injected/src/ariaSnapshot.ts @@ -31,6 +31,8 @@ export type AriaNode = AriaProps & { element: Element; box: Box; receivesPointerEvents: boolean; + focused?: boolean; + focusable?: boolean; props: Record; }; @@ -192,7 +194,9 @@ function toAriaNode(element: Element, options?: { forAI?: boolean, refPrefix?: s props: {}, element, box: box(element), - receivesPointerEvents + receivesPointerEvents, + focused: element.ownerDocument.activeElement === element, + focusable: roleUtils.isFocusable(element) }; if (roleUtils.kAriaCheckedRoles.includes(role)) @@ -431,6 +435,10 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, options?: { mode?: 'r key += ` [disabled]`; if (ariaNode.expanded) key += ` [expanded]`; + if (ariaNode.focused) + key += ` [focused]`; + if (ariaNode.focusable) + key += ` [focusable]`; if (ariaNode.level) key += ` [level=${ariaNode.level}]`; if (ariaNode.pressed === 'mixed') diff --git a/packages/injected/src/roleUtils.ts b/packages/injected/src/roleUtils.ts index d1e2c91f49c8f..67da181cc83ab 100644 --- a/packages/injected/src/roleUtils.ts +++ b/packages/injected/src/roleUtils.ts @@ -66,7 +66,7 @@ function hasTabIndex(element: Element) { return !Number.isNaN(Number(String(element.getAttribute('tabindex')))); } -function isFocusable(element: Element) { +export function isFocusable(element: Element) { // TODO: // - "inert" attribute makes the whole substree not focusable // - when dialog is open on the page - everything but the dialog is not focusable diff --git a/packages/playwright-core/src/server/chromium/crAccessibility.ts b/packages/playwright-core/src/server/chromium/crAccessibility.ts index 539f3057c72a5..849c5e2bea42f 100644 --- a/packages/playwright-core/src/server/chromium/crAccessibility.ts +++ b/packages/playwright-core/src/server/chromium/crAccessibility.ts @@ -254,6 +254,9 @@ class CRAXNode implements accessibility.AXNode { continue; node[booleanProperty] = value; } + + // Add focusable property + node.focusable = this._focusable; const numericalProperties: Array = [ 'level', 'valuemax', diff --git a/packages/playwright-core/src/server/firefox/ffAccessibility.ts b/packages/playwright-core/src/server/firefox/ffAccessibility.ts index d8d56b58de295..ffa943461a1c8 100644 --- a/packages/playwright-core/src/server/firefox/ffAccessibility.ts +++ b/packages/playwright-core/src/server/firefox/ffAccessibility.ts @@ -234,6 +234,10 @@ class FFAXNode implements accessibility.AXNode { continue; node[booleanProperty] = value; } + + // Add focusable property + node.focusable = this._focusable; + const numericalProperties: Array = [ 'level' ]; diff --git a/packages/playwright-core/src/server/webkit/wkAccessibility.ts b/packages/playwright-core/src/server/webkit/wkAccessibility.ts index 52b9199df55bc..7f272af6ea57f 100644 --- a/packages/playwright-core/src/server/webkit/wkAccessibility.ts +++ b/packages/playwright-core/src/server/webkit/wkAccessibility.ts @@ -225,6 +225,9 @@ class WKAXNode implements accessibility.AXNode { continue; (node as any)[booleanProperty] = value; } + + // Add focusable property + node.focusable = this._payload.focusable; const numericalProperties: Array = [ 'level', diff --git a/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts b/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts index ffbd50ddef416..7407e8fc27894 100644 --- a/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts +++ b/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts @@ -28,6 +28,8 @@ export type AriaProps = { checked?: boolean | 'mixed'; disabled?: boolean; expanded?: boolean; + focused?: boolean; + focusable?: boolean; level?: number; pressed?: boolean | 'mixed'; selected?: boolean; @@ -443,6 +445,16 @@ export class KeyParser { node.expanded = value === 'true'; return; } + if (key === 'focused') { + this._assert(value === 'true' || value === 'false', 'Value of "focused" attribute must be a boolean', errorPos); + node.focused = value === 'true'; + return; + } + if (key === 'focusable') { + this._assert(value === 'true' || value === 'false', 'Value of "focusable" attribute must be a boolean', errorPos); + node.focusable = value === 'true'; + return; + } if (key === 'level') { this._assert(!isNaN(Number(value)), 'Value of "level" attribute must be a number', errorPos); node.level = Number(value); diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index c00b3e2c3e33c..304409e81132e 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -231,3 +231,27 @@ it('should gracefully fallback when child frame cant be captured', async ({ page - iframe [ref=e3] `); }); + +it('should include focused and focusable information', async ({ page }) => { + await page.setContent(` + + +
Not focusable
+ `); + + const snapshot = await snapshotForAI(page); + + // Check for Button 1 with focusable attribute + expect(snapshot).toContain('button "Button 1"'); + expect(snapshot).toContain('[focusable]'); + + // Check for Button 2 with focusable attribute + expect(snapshot).toContain('button "Button 2"'); + expect(snapshot).toMatch(/Button 2.*\[focusable\]/); + + // Check that there's a focused element somewhere in the snapshot + expect(snapshot).toContain('[focused]'); + + // Check for the non-focusable div + expect(snapshot).toContain('Not focusable'); +}); From 77835061b90a1933959f4f091e06ca5dbbef145c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 May 2025 01:15:08 +0000 Subject: [PATCH 03/13] Replace focusable/focused with active attribute in aria snapshot Co-authored-by: yury-s <9798949+yury-s@users.noreply.github.com> --- packages/injected/src/ariaSnapshot.ts | 12 ++++-------- packages/injected/src/roleUtils.ts | 2 +- .../src/server/chromium/crAccessibility.ts | 3 --- .../src/server/firefox/ffAccessibility.ts | 4 ---- .../src/server/webkit/wkAccessibility.ts | 3 --- .../src/utils/isomorphic/ariaSnapshot.ts | 14 ++++---------- tests/page/page-aria-snapshot-ai.spec.ts | 16 +++++++--------- 7 files changed, 16 insertions(+), 38 deletions(-) diff --git a/packages/injected/src/ariaSnapshot.ts b/packages/injected/src/ariaSnapshot.ts index 0f03f252f8cb8..4776f89866a9e 100644 --- a/packages/injected/src/ariaSnapshot.ts +++ b/packages/injected/src/ariaSnapshot.ts @@ -31,8 +31,7 @@ export type AriaNode = AriaProps & { element: Element; box: Box; receivesPointerEvents: boolean; - focused?: boolean; - focusable?: boolean; + active?: boolean; props: Record; }; @@ -195,8 +194,7 @@ function toAriaNode(element: Element, options?: { forAI?: boolean, refPrefix?: s element, box: box(element), receivesPointerEvents, - focused: element.ownerDocument.activeElement === element, - focusable: roleUtils.isFocusable(element) + active: element.ownerDocument.activeElement === element }; if (roleUtils.kAriaCheckedRoles.includes(role)) @@ -435,10 +433,8 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, options?: { mode?: 'r key += ` [disabled]`; if (ariaNode.expanded) key += ` [expanded]`; - if (ariaNode.focused) - key += ` [focused]`; - if (ariaNode.focusable) - key += ` [focusable]`; + if (ariaNode.active) + key += ` [active]`; if (ariaNode.level) key += ` [level=${ariaNode.level}]`; if (ariaNode.pressed === 'mixed') diff --git a/packages/injected/src/roleUtils.ts b/packages/injected/src/roleUtils.ts index 67da181cc83ab..d1e2c91f49c8f 100644 --- a/packages/injected/src/roleUtils.ts +++ b/packages/injected/src/roleUtils.ts @@ -66,7 +66,7 @@ function hasTabIndex(element: Element) { return !Number.isNaN(Number(String(element.getAttribute('tabindex')))); } -export function isFocusable(element: Element) { +function isFocusable(element: Element) { // TODO: // - "inert" attribute makes the whole substree not focusable // - when dialog is open on the page - everything but the dialog is not focusable diff --git a/packages/playwright-core/src/server/chromium/crAccessibility.ts b/packages/playwright-core/src/server/chromium/crAccessibility.ts index 849c5e2bea42f..539f3057c72a5 100644 --- a/packages/playwright-core/src/server/chromium/crAccessibility.ts +++ b/packages/playwright-core/src/server/chromium/crAccessibility.ts @@ -254,9 +254,6 @@ class CRAXNode implements accessibility.AXNode { continue; node[booleanProperty] = value; } - - // Add focusable property - node.focusable = this._focusable; const numericalProperties: Array = [ 'level', 'valuemax', diff --git a/packages/playwright-core/src/server/firefox/ffAccessibility.ts b/packages/playwright-core/src/server/firefox/ffAccessibility.ts index ffa943461a1c8..d8d56b58de295 100644 --- a/packages/playwright-core/src/server/firefox/ffAccessibility.ts +++ b/packages/playwright-core/src/server/firefox/ffAccessibility.ts @@ -234,10 +234,6 @@ class FFAXNode implements accessibility.AXNode { continue; node[booleanProperty] = value; } - - // Add focusable property - node.focusable = this._focusable; - const numericalProperties: Array = [ 'level' ]; diff --git a/packages/playwright-core/src/server/webkit/wkAccessibility.ts b/packages/playwright-core/src/server/webkit/wkAccessibility.ts index 7f272af6ea57f..52b9199df55bc 100644 --- a/packages/playwright-core/src/server/webkit/wkAccessibility.ts +++ b/packages/playwright-core/src/server/webkit/wkAccessibility.ts @@ -225,9 +225,6 @@ class WKAXNode implements accessibility.AXNode { continue; (node as any)[booleanProperty] = value; } - - // Add focusable property - node.focusable = this._payload.focusable; const numericalProperties: Array = [ 'level', diff --git a/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts b/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts index 7407e8fc27894..2e92b67d1c994 100644 --- a/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts +++ b/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts @@ -28,8 +28,7 @@ export type AriaProps = { checked?: boolean | 'mixed'; disabled?: boolean; expanded?: boolean; - focused?: boolean; - focusable?: boolean; + active?: boolean; level?: number; pressed?: boolean | 'mixed'; selected?: boolean; @@ -445,14 +444,9 @@ export class KeyParser { node.expanded = value === 'true'; return; } - if (key === 'focused') { - this._assert(value === 'true' || value === 'false', 'Value of "focused" attribute must be a boolean', errorPos); - node.focused = value === 'true'; - return; - } - if (key === 'focusable') { - this._assert(value === 'true' || value === 'false', 'Value of "focusable" attribute must be a boolean', errorPos); - node.focusable = value === 'true'; + if (key === 'active') { + this._assert(value === 'true' || value === 'false', 'Value of "active" attribute must be a boolean', errorPos); + node.active = value === 'true'; return; } if (key === 'level') { diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index 304409e81132e..13539d7e96466 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -232,7 +232,7 @@ it('should gracefully fallback when child frame cant be captured', async ({ page `); }); -it('should include focused and focusable information', async ({ page }) => { +it('should include active element information', async ({ page }) => { await page.setContent(` @@ -241,17 +241,15 @@ it('should include focused and focusable information', async ({ page }) => { const snapshot = await snapshotForAI(page); - // Check for Button 1 with focusable attribute + // Check for Button 1 expect(snapshot).toContain('button "Button 1"'); - expect(snapshot).toContain('[focusable]'); - - // Check for Button 2 with focusable attribute + + // Check for Button 2 expect(snapshot).toContain('button "Button 2"'); - expect(snapshot).toMatch(/Button 2.*\[focusable\]/); - // Check that there's a focused element somewhere in the snapshot - expect(snapshot).toContain('[focused]'); + // Check that there's an active element somewhere in the snapshot + expect(snapshot).toContain('[active]'); - // Check for the non-focusable div + // Check for the div expect(snapshot).toContain('Not focusable'); }); From 647396472a500876e45312b2a7477f3e157642e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 May 2025 18:06:07 +0000 Subject: [PATCH 04/13] fix: updated tests for active element in AI snapshots Co-authored-by: yury-s <9798949+yury-s@users.noreply.github.com> --- tests/page/page-aria-snapshot-ai.spec.ts | 40 +++++++++++++++++++----- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index 13539d7e96466..421fafae9f704 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -241,15 +241,41 @@ it('should include active element information', async ({ page }) => { const snapshot = await snapshotForAI(page); - // Check for Button 1 + // Check for all buttons and text expect(snapshot).toContain('button "Button 1"'); - - // Check for Button 2 expect(snapshot).toContain('button "Button 2"'); + expect(snapshot).toContain('Not focusable'); + + // Check that active element is marked + expect(snapshot).toMatch(/\[active\]/); +}); + +it('should update active element on focus', async ({ page }) => { + await page.setContent(` + + + `); + + // Initially there shouldn't be an active element on the inputs + const initialSnapshot = await snapshotForAI(page); + expect(initialSnapshot).toContain('textbox "First input"'); + expect(initialSnapshot).toContain('textbox "Second input"'); - // Check that there's an active element somewhere in the snapshot - expect(snapshot).toContain('[active]'); + // Focus the second input + await page.locator('#input2').focus(); - // Check for the div - expect(snapshot).toContain('Not focusable'); + // After focus, the second input should be active + const afterFocusSnapshot = await snapshotForAI(page); + + // Verify it contains both inputs + expect(afterFocusSnapshot).toContain('textbox "First input"'); + expect(afterFocusSnapshot).toContain('textbox "Second input"'); + + // Only one of these will pass depending on browser implementation: + // Either the second input is marked as active + const secondInputActive = afterFocusSnapshot.includes('textbox "Second input" [active]'); + // Or the document is no longer marked as active (because input has focus) + const docNotActive = !afterFocusSnapshot.includes('generic [active]'); + + expect(secondInputActive || docNotActive).toBeTruthy(); }); From 4a40ed7c080dcda5643e4a8d4b92fcef339f5ebb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 May 2025 18:12:31 +0000 Subject: [PATCH 05/13] fix: remove trailing whitespace from test file Co-authored-by: yury-s <9798949+yury-s@users.noreply.github.com> --- tests/page/page-aria-snapshot-ai.spec.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index 421fafae9f704..42f84abe23159 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -240,12 +240,12 @@ it('should include active element information', async ({ page }) => { `); const snapshot = await snapshotForAI(page); - + // Check for all buttons and text expect(snapshot).toContain('button "Button 1"'); expect(snapshot).toContain('button "Button 2"'); expect(snapshot).toContain('Not focusable'); - + // Check that active element is marked expect(snapshot).toMatch(/\[active\]/); }); @@ -260,22 +260,22 @@ it('should update active element on focus', async ({ page }) => { const initialSnapshot = await snapshotForAI(page); expect(initialSnapshot).toContain('textbox "First input"'); expect(initialSnapshot).toContain('textbox "Second input"'); - + // Focus the second input await page.locator('#input2').focus(); - + // After focus, the second input should be active const afterFocusSnapshot = await snapshotForAI(page); - + // Verify it contains both inputs expect(afterFocusSnapshot).toContain('textbox "First input"'); expect(afterFocusSnapshot).toContain('textbox "Second input"'); - + // Only one of these will pass depending on browser implementation: // Either the second input is marked as active const secondInputActive = afterFocusSnapshot.includes('textbox "Second input" [active]'); // Or the document is no longer marked as active (because input has focus) const docNotActive = !afterFocusSnapshot.includes('generic [active]'); - + expect(secondInputActive || docNotActive).toBeTruthy(); }); From e9746cc4562f07199751798b8b35c21bacd8e83d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 May 2025 20:19:40 +0000 Subject: [PATCH 06/13] fix: make active element tests use toContainYaml and be deterministic Co-authored-by: yury-s <9798949+yury-s@users.noreply.github.com> --- tests/page/page-aria-snapshot-ai.spec.ts | 45 ++++++++++-------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index 42f84abe23159..9855222001208 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -29,7 +29,7 @@ it('should generate refs', async ({ page }) => { const snapshot1 = await snapshotForAI(page); expect(snapshot1).toContainYaml(` - - generic [ref=e1]: + - generic [active] [ref=e1]: - button "One" [ref=e2] - button "Two" [ref=e3] - button "Three" [ref=e4] @@ -44,7 +44,7 @@ it('should generate refs', async ({ page }) => { const snapshot2 = await snapshotForAI(page); expect(snapshot2).toContainYaml(` - - generic [ref=e1]: + - generic [active] [ref=e1]: - button "One" [ref=e2] - button "Not Two" [ref=e5] - button "Three" [ref=e4] @@ -68,9 +68,9 @@ it('should stitch all frame snapshots', async ({ page, server }) => { await page.goto(server.PREFIX + '/frames/nested-frames.html'); const snapshot = await snapshotForAI(page); expect(snapshot).toContainYaml(` - - generic [ref=e1]: + - generic [active] [ref=e1]: - iframe [ref=e2]: - - generic [ref=f1e1]: + - generic [active] [ref=f1e1]: - iframe [ref=f1e2]: - generic [ref=f2e2]: Hi, I'm frame - iframe [ref=f1e3]: @@ -98,7 +98,7 @@ it('should not generate refs for hidden elements', async ({ page }) => { const snapshot = await snapshotForAI(page); expect(snapshot).toContainYaml(` - - generic [ref=e1]: + - generic [active] [ref=e1]: - button "One" [ref=e2] - button "Two" - button "Three" [ref=e4] @@ -130,7 +130,7 @@ it('should not generate refs for elements with pointer-events:none', async ({ pa const snapshot = await snapshotForAI(page); expect(snapshot).toContainYaml(` - - generic [ref=e1]: + - generic [active] [ref=e1]: - button "no-ref" - button "with-ref" [ref=e4] - button "with-ref" [ref=e7] @@ -226,7 +226,7 @@ it('should gracefully fallback when child frame cant be captured', async ({ page `, { waitUntil: 'domcontentloaded' }); const snapshot = await snapshotForAI(page); expect(snapshot).toContainYaml(` - - generic [ref=e1]: + - generic [active] [ref=e1]: - paragraph [ref=e2]: Test - iframe [ref=e3] `); @@ -241,13 +241,12 @@ it('should include active element information', async ({ page }) => { const snapshot = await snapshotForAI(page); - // Check for all buttons and text - expect(snapshot).toContain('button "Button 1"'); - expect(snapshot).toContain('button "Button 2"'); - expect(snapshot).toContain('Not focusable'); - - // Check that active element is marked - expect(snapshot).toMatch(/\[active\]/); + expect(snapshot).toContainYaml(` + - generic [ref=e1]: + - button "Button 1" [ref=e2] + - button "Button 2" [active] [ref=e3] + - generic [ref=e4]: Not focusable + `); }); it('should update active element on focus', async ({ page }) => { @@ -266,16 +265,10 @@ it('should update active element on focus', async ({ page }) => { // After focus, the second input should be active const afterFocusSnapshot = await snapshotForAI(page); - - // Verify it contains both inputs - expect(afterFocusSnapshot).toContain('textbox "First input"'); - expect(afterFocusSnapshot).toContain('textbox "Second input"'); - - // Only one of these will pass depending on browser implementation: - // Either the second input is marked as active - const secondInputActive = afterFocusSnapshot.includes('textbox "Second input" [active]'); - // Or the document is no longer marked as active (because input has focus) - const docNotActive = !afterFocusSnapshot.includes('generic [active]'); - - expect(secondInputActive || docNotActive).toBeTruthy(); + + expect(afterFocusSnapshot).toContainYaml(` + - generic [ref=e1]: + - textbox "First input" [ref=e2] + - textbox "Second input" [active] [ref=e3] + `); }); From d14a0c100c25f2d4b99421e576718ba5a49f060a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 May 2025 20:52:25 +0000 Subject: [PATCH 07/13] test: replace individual toContain calls with toContainYaml for consistency Co-authored-by: yury-s <9798949+yury-s@users.noreply.github.com> --- tests/page/page-aria-snapshot-ai.spec.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index 9855222001208..103c589ed845e 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -257,8 +257,11 @@ it('should update active element on focus', async ({ page }) => { // Initially there shouldn't be an active element on the inputs const initialSnapshot = await snapshotForAI(page); - expect(initialSnapshot).toContain('textbox "First input"'); - expect(initialSnapshot).toContain('textbox "Second input"'); + expect(initialSnapshot).toContainYaml(` + - generic [active] [ref=e1]: + - textbox "First input" [ref=e2] + - textbox "Second input" [ref=e3] + `); // Focus the second input await page.locator('#input2').focus(); From 5056a57d5ea9b93f704ecd1b8ea17330a46af1c5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 May 2025 21:10:49 +0000 Subject: [PATCH 08/13] feat: add active element detection for iframes in AI snapshots Co-authored-by: yury-s <9798949+yury-s@users.noreply.github.com> --- packages/injected/src/ariaSnapshot.ts | 17 +++++++++++++++-- tests/page/page-aria-snapshot-ai.spec.ts | 16 ++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/injected/src/ariaSnapshot.ts b/packages/injected/src/ariaSnapshot.ts index 4776f89866a9e..7f3ba124a19d1 100644 --- a/packages/injected/src/ariaSnapshot.ts +++ b/packages/injected/src/ariaSnapshot.ts @@ -165,15 +165,28 @@ function ariaRef(element: Element, role: string, name: string, options?: { forAI function toAriaNode(element: Element, options?: { forAI?: boolean, refPrefix?: string }): AriaNode | null { if (element.nodeName === 'IFRAME') { + const iframe = element as HTMLIFrameElement; + const isDirectlyActive = element.ownerDocument.activeElement === element; + let containsActiveElement = false; + try { + containsActiveElement = iframe.contentDocument && + iframe.contentDocument.activeElement && + iframe.contentDocument.activeElement !== iframe.contentDocument.body; + } catch (e) { + // Cross-origin iframe, can't access contentDocument + } + const isActive = isDirectlyActive || !!containsActiveElement; + return { - role: 'iframe', + role: 'iframe' as const, name: '', ref: ariaRef(element, 'iframe', '', options), children: [], props: {}, element, box: box(element), - receivesPointerEvents: true + receivesPointerEvents: true, + active: isActive }; } diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index 103c589ed845e..b57a5d474a966 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -275,3 +275,19 @@ it('should update active element on focus', async ({ page }) => { - textbox "Second input" [active] [ref=e3] `); }); + +it('should mark iframe as active when it contains focused element', async ({ page }) => { + // Create a simple HTML file for the iframe + await page.setContent(` + + + `); + + // Test 1: Focus the input inside the iframe + await page.frameLocator('iframe').locator('#iframe-input').focus(); + const inputInIframeFocusedSnapshot = await snapshotForAI(page); + console.log('Input in iframe focused snapshot:', inputInIframeFocusedSnapshot); + + // The iframe should be marked as active when it contains a focused element + expect(inputInIframeFocusedSnapshot).toContain('iframe [active]'); +}); From 44bc50b9b4acf61f7a1f6d447b8d521070eb35a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 May 2025 21:52:10 +0000 Subject: [PATCH 09/13] fix: simplify iframe active detection and improve test assertions Co-authored-by: yury-s <9798949+yury-s@users.noreply.github.com> --- tests/page/page-aria-snapshot-ai.spec.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index b57a5d474a966..581fe0640f79f 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -280,14 +280,17 @@ it('should mark iframe as active when it contains focused element', async ({ pag // Create a simple HTML file for the iframe await page.setContent(` - + `); // Test 1: Focus the input inside the iframe await page.frameLocator('iframe').locator('#iframe-input').focus(); const inputInIframeFocusedSnapshot = await snapshotForAI(page); - console.log('Input in iframe focused snapshot:', inputInIframeFocusedSnapshot); // The iframe should be marked as active when it contains a focused element expect(inputInIframeFocusedSnapshot).toContain('iframe [active]'); + + // Also check that the input element inside the iframe is active + const iframeSnapshot = await page.frameLocator('iframe').locator('body').ariaSnapshot(); + expect(iframeSnapshot).toContain('textbox "Input in iframe" [active]'); }); From 74d6d982f7e5d5512370af96dfd57dd723876e91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 May 2025 23:23:58 +0000 Subject: [PATCH 10/13] fix: simplify iframe active detection and fix autofocus timing in tests Co-authored-by: yury-s <9798949+yury-s@users.noreply.github.com> --- packages/injected/src/ariaSnapshot.ts | 16 +++------------- tests/page/page-aria-snapshot-ai.spec.ts | 9 ++++++--- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/packages/injected/src/ariaSnapshot.ts b/packages/injected/src/ariaSnapshot.ts index 7f3ba124a19d1..6ea22431d43ef 100644 --- a/packages/injected/src/ariaSnapshot.ts +++ b/packages/injected/src/ariaSnapshot.ts @@ -165,20 +165,10 @@ function ariaRef(element: Element, role: string, name: string, options?: { forAI function toAriaNode(element: Element, options?: { forAI?: boolean, refPrefix?: string }): AriaNode | null { if (element.nodeName === 'IFRAME') { - const iframe = element as HTMLIFrameElement; - const isDirectlyActive = element.ownerDocument.activeElement === element; - let containsActiveElement = false; - try { - containsActiveElement = iframe.contentDocument && - iframe.contentDocument.activeElement && - iframe.contentDocument.activeElement !== iframe.contentDocument.body; - } catch (e) { - // Cross-origin iframe, can't access contentDocument - } - const isActive = isDirectlyActive || !!containsActiveElement; - + const isActive = element.ownerDocument.activeElement === element; + return { - role: 'iframe' as const, + role: 'iframe', name: '', ref: ariaRef(element, 'iframe', '', options), children: [], diff --git a/tests/page/page-aria-snapshot-ai.spec.ts b/tests/page/page-aria-snapshot-ai.spec.ts index 1c49362e8d56d..0d8f90a2cc10e 100644 --- a/tests/page/page-aria-snapshot-ai.spec.ts +++ b/tests/page/page-aria-snapshot-ai.spec.ts @@ -237,6 +237,9 @@ it('should include active element information', async ({ page }) => {
Not focusable
`); + // Wait for autofocus to take effect + await page.waitForFunction(() => document.activeElement?.id === 'btn2'); + const snapshot = await snapshotForAI(page); expect(snapshot).toContainYaml(` @@ -266,7 +269,7 @@ it('should update active element on focus', async ({ page }) => { // After focus, the second input should be active const afterFocusSnapshot = await snapshotForAI(page); - + expect(afterFocusSnapshot).toContainYaml(` - generic [ref=e1]: - textbox "First input" [ref=e2] @@ -284,10 +287,10 @@ it('should mark iframe as active when it contains focused element', async ({ pag // Test 1: Focus the input inside the iframe await page.frameLocator('iframe').locator('#iframe-input').focus(); const inputInIframeFocusedSnapshot = await snapshotForAI(page); - + // The iframe should be marked as active when it contains a focused element expect(inputInIframeFocusedSnapshot).toContain('iframe [active]'); - + // Also check that the input element inside the iframe is active const iframeSnapshot = await page.frameLocator('iframe').locator('body').ariaSnapshot(); expect(iframeSnapshot).toContain('textbox "Input in iframe" [active]'); From 002a235f1e50e552e9a36d74487d5326fa59f5eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 May 2025 21:08:08 +0000 Subject: [PATCH 11/13] test: update cli-codegen-aria expectations to include [active] attribute Co-authored-by: yury-s <9798949+yury-s@users.noreply.github.com> --- .../inspector/cli-codegen-aria.spec.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/library/inspector/cli-codegen-aria.spec.ts b/tests/library/inspector/cli-codegen-aria.spec.ts index a11cf333422e8..d67e2e09c22c5 100644 --- a/tests/library/inspector/cli-codegen-aria.spec.ts +++ b/tests/library/inspector/cli-codegen-aria.spec.ts @@ -29,15 +29,15 @@ test.describe(() => { await recorder.trustedClick(); await expect.poll(() => - recorder.text('JavaScript')).toContain(`await expect(page.getByRole('button')).toMatchAriaSnapshot(\`- button "Submit"\`);`); + recorder.text('JavaScript')).toContain(`await expect(page.getByRole('button')).toMatchAriaSnapshot(\`- button "Submit" [active]\`);`); await expect.poll(() => - recorder.text('Python')).toContain(`expect(page.get_by_role("button")).to_match_aria_snapshot("- button \\"Submit\\"")`); + recorder.text('Python')).toContain(`expect(page.get_by_role("button")).to_match_aria_snapshot("- button \\"Submit\\" [active]")`); await expect.poll(() => - recorder.text('Python Async')).toContain(`await expect(page.get_by_role(\"button\")).to_match_aria_snapshot("- button \\"Submit\\"")`); + recorder.text('Python Async')).toContain(`await expect(page.get_by_role(\"button\")).to_match_aria_snapshot("- button \\"Submit\\" [active]")`); await expect.poll(() => - recorder.text('Java')).toContain(`assertThat(page.getByRole(AriaRole.BUTTON)).matchesAriaSnapshot("- button \\"Submit\\"");`); + recorder.text('Java')).toContain(`assertThat(page.getByRole(AriaRole.BUTTON)).matchesAriaSnapshot("- button \\"Submit\\" [active]");`); await expect.poll(() => - recorder.text('C#')).toContain(`await Expect(page.GetByRole(AriaRole.Button)).ToMatchAriaSnapshotAsync("- button \\"Submit\\"");`); + recorder.text('C#')).toContain(`await Expect(page.GetByRole(AriaRole.Button)).ToMatchAriaSnapshotAsync("- button \\"Submit\\" [active]");`); }); test('should generate regex in aria snapshot', async ({ openRecorder }) => { @@ -49,15 +49,15 @@ test.describe(() => { await recorder.trustedClick(); await expect.poll(() => - recorder.text('JavaScript')).toContain(`await expect(page.getByRole('button')).toMatchAriaSnapshot(\`- button /Submit \\\\d+/\`);`); + recorder.text('JavaScript')).toContain(`await expect(page.getByRole('button')).toMatchAriaSnapshot(\`- button /Submit \\\\d+/ [active]\`);`); await expect.poll(() => - recorder.text('Python')).toContain(`expect(page.get_by_role("button")).to_match_aria_snapshot("- button /Submit \\\\d+/")`); + recorder.text('Python')).toContain(`expect(page.get_by_role("button")).to_match_aria_snapshot("- button /Submit \\\\d+/ [active]")`); await expect.poll(() => - recorder.text('Python Async')).toContain(`await expect(page.get_by_role(\"button\")).to_match_aria_snapshot("- button /Submit \\\\d+/")`); + recorder.text('Python Async')).toContain(`await expect(page.get_by_role(\"button\")).to_match_aria_snapshot("- button /Submit \\\\d+/ [active]")`); await expect.poll(() => - recorder.text('Java')).toContain(`assertThat(page.getByRole(AriaRole.BUTTON)).matchesAriaSnapshot("- button /Submit \\\\d+/");`); + recorder.text('Java')).toContain(`assertThat(page.getByRole(AriaRole.BUTTON)).matchesAriaSnapshot("- button /Submit \\\\d+/ [active]");`); await expect.poll(() => - recorder.text('C#')).toContain(`await Expect(page.GetByRole(AriaRole.Button)).ToMatchAriaSnapshotAsync("- button /Submit \\\\d+/");`); + recorder.text('C#')).toContain(`await Expect(page.GetByRole(AriaRole.Button)).ToMatchAriaSnapshotAsync("- button /Submit \\\\d+/ [active]");`); }); test('should inspect aria snapshot', async ({ openRecorder }) => { From a7f952f675c14576ee4e50faf5de4316e5f5f390 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Jun 2025 17:59:10 +0000 Subject: [PATCH 12/13] fix: move active property to AriaProps and add codegen restrictions Co-authored-by: pavelfeldman <883973+pavelfeldman@users.noreply.github.com> --- packages/injected/src/ariaSnapshot.ts | 3 +- .../inspector/cli-codegen-aria.spec.ts | 20 +++--- .../to-match-aria-snapshot-active.spec.ts | 67 +++++++++++++++++++ 3 files changed, 78 insertions(+), 12 deletions(-) create mode 100644 tests/page/to-match-aria-snapshot-active.spec.ts diff --git a/packages/injected/src/ariaSnapshot.ts b/packages/injected/src/ariaSnapshot.ts index 6ea22431d43ef..2390e77bd616f 100644 --- a/packages/injected/src/ariaSnapshot.ts +++ b/packages/injected/src/ariaSnapshot.ts @@ -31,7 +31,6 @@ export type AriaNode = AriaProps & { element: Element; box: Box; receivesPointerEvents: boolean; - active?: boolean; props: Record; }; @@ -436,7 +435,7 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, options?: { mode?: 'r key += ` [disabled]`; if (ariaNode.expanded) key += ` [expanded]`; - if (ariaNode.active) + if (ariaNode.active && options?.forAI) key += ` [active]`; if (ariaNode.level) key += ` [level=${ariaNode.level}]`; diff --git a/tests/library/inspector/cli-codegen-aria.spec.ts b/tests/library/inspector/cli-codegen-aria.spec.ts index d67e2e09c22c5..a11cf333422e8 100644 --- a/tests/library/inspector/cli-codegen-aria.spec.ts +++ b/tests/library/inspector/cli-codegen-aria.spec.ts @@ -29,15 +29,15 @@ test.describe(() => { await recorder.trustedClick(); await expect.poll(() => - recorder.text('JavaScript')).toContain(`await expect(page.getByRole('button')).toMatchAriaSnapshot(\`- button "Submit" [active]\`);`); + recorder.text('JavaScript')).toContain(`await expect(page.getByRole('button')).toMatchAriaSnapshot(\`- button "Submit"\`);`); await expect.poll(() => - recorder.text('Python')).toContain(`expect(page.get_by_role("button")).to_match_aria_snapshot("- button \\"Submit\\" [active]")`); + recorder.text('Python')).toContain(`expect(page.get_by_role("button")).to_match_aria_snapshot("- button \\"Submit\\"")`); await expect.poll(() => - recorder.text('Python Async')).toContain(`await expect(page.get_by_role(\"button\")).to_match_aria_snapshot("- button \\"Submit\\" [active]")`); + recorder.text('Python Async')).toContain(`await expect(page.get_by_role(\"button\")).to_match_aria_snapshot("- button \\"Submit\\"")`); await expect.poll(() => - recorder.text('Java')).toContain(`assertThat(page.getByRole(AriaRole.BUTTON)).matchesAriaSnapshot("- button \\"Submit\\" [active]");`); + recorder.text('Java')).toContain(`assertThat(page.getByRole(AriaRole.BUTTON)).matchesAriaSnapshot("- button \\"Submit\\"");`); await expect.poll(() => - recorder.text('C#')).toContain(`await Expect(page.GetByRole(AriaRole.Button)).ToMatchAriaSnapshotAsync("- button \\"Submit\\" [active]");`); + recorder.text('C#')).toContain(`await Expect(page.GetByRole(AriaRole.Button)).ToMatchAriaSnapshotAsync("- button \\"Submit\\"");`); }); test('should generate regex in aria snapshot', async ({ openRecorder }) => { @@ -49,15 +49,15 @@ test.describe(() => { await recorder.trustedClick(); await expect.poll(() => - recorder.text('JavaScript')).toContain(`await expect(page.getByRole('button')).toMatchAriaSnapshot(\`- button /Submit \\\\d+/ [active]\`);`); + recorder.text('JavaScript')).toContain(`await expect(page.getByRole('button')).toMatchAriaSnapshot(\`- button /Submit \\\\d+/\`);`); await expect.poll(() => - recorder.text('Python')).toContain(`expect(page.get_by_role("button")).to_match_aria_snapshot("- button /Submit \\\\d+/ [active]")`); + recorder.text('Python')).toContain(`expect(page.get_by_role("button")).to_match_aria_snapshot("- button /Submit \\\\d+/")`); await expect.poll(() => - recorder.text('Python Async')).toContain(`await expect(page.get_by_role(\"button\")).to_match_aria_snapshot("- button /Submit \\\\d+/ [active]")`); + recorder.text('Python Async')).toContain(`await expect(page.get_by_role(\"button\")).to_match_aria_snapshot("- button /Submit \\\\d+/")`); await expect.poll(() => - recorder.text('Java')).toContain(`assertThat(page.getByRole(AriaRole.BUTTON)).matchesAriaSnapshot("- button /Submit \\\\d+/ [active]");`); + recorder.text('Java')).toContain(`assertThat(page.getByRole(AriaRole.BUTTON)).matchesAriaSnapshot("- button /Submit \\\\d+/");`); await expect.poll(() => - recorder.text('C#')).toContain(`await Expect(page.GetByRole(AriaRole.Button)).ToMatchAriaSnapshotAsync("- button /Submit \\\\d+/ [active]");`); + recorder.text('C#')).toContain(`await Expect(page.GetByRole(AriaRole.Button)).ToMatchAriaSnapshotAsync("- button /Submit \\\\d+/");`); }); test('should inspect aria snapshot', async ({ openRecorder }) => { diff --git a/tests/page/to-match-aria-snapshot-active.spec.ts b/tests/page/to-match-aria-snapshot-active.spec.ts new file mode 100644 index 0000000000000..bfde62b51c605 --- /dev/null +++ b/tests/page/to-match-aria-snapshot-active.spec.ts @@ -0,0 +1,67 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './pageTest'; + +test('should match active element', async ({ page }) => { + await page.setContent(` + + + `); + + // Wait for autofocus to take effect + await page.waitForFunction(() => document.activeElement?.id === 'btn2'); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - button "Button 1" + - button "Button 2" [active] + `); +}); + +test('should match active element after focus', async ({ page }) => { + await page.setContent(` + + + `); + + // Focus the second input + await page.locator('#input2').focus(); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - textbox "First input" + - textbox "Second input" [active] + `); +}); + +test('should match active iframe', async ({ page }) => { + await page.setContent(` + + + `); + + // Focus the input inside the iframe + await page.frameLocator('iframe').locator('#iframe-input').focus(); + + await expect(page.locator('body')).toMatchAriaSnapshot(` + - textbox "Regular input" + - iframe [active] + `); + + // Also check that the input element inside the iframe is active + await expect(page.frameLocator('iframe').locator('body')).toMatchAriaSnapshot(` + - textbox "Input in iframe" [active] + `); +}); \ No newline at end of file From cfb46a7b72926874a3850dc5c0cdda915e716547 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Jun 2025 18:06:07 +0000 Subject: [PATCH 13/13] fix: remove [active] from codegen snapshots in all languages Co-authored-by: pavelfeldman <883973+pavelfeldman@users.noreply.github.com> --- packages/injected/src/ariaSnapshot.ts | 2 +- packages/playwright-core/src/server/codegen/csharp.ts | 4 +++- packages/playwright-core/src/server/codegen/java.ts | 4 +++- packages/playwright-core/src/server/codegen/javascript.ts | 4 +++- packages/playwright-core/src/server/codegen/python.ts | 4 +++- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/injected/src/ariaSnapshot.ts b/packages/injected/src/ariaSnapshot.ts index 2390e77bd616f..a12ff0b896f7e 100644 --- a/packages/injected/src/ariaSnapshot.ts +++ b/packages/injected/src/ariaSnapshot.ts @@ -435,7 +435,7 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, options?: { mode?: 'r key += ` [disabled]`; if (ariaNode.expanded) key += ` [expanded]`; - if (ariaNode.active && options?.forAI) + if (ariaNode.active) key += ` [active]`; if (ariaNode.level) key += ` [level=${ariaNode.level}]`; diff --git a/packages/playwright-core/src/server/codegen/csharp.ts b/packages/playwright-core/src/server/codegen/csharp.ts index 9fd67fa96c06c..77ef62924db54 100644 --- a/packages/playwright-core/src/server/codegen/csharp.ts +++ b/packages/playwright-core/src/server/codegen/csharp.ts @@ -148,7 +148,9 @@ export class CSharpLanguageGenerator implements LanguageGenerator { return `await Expect(${subject}.${this._asLocator(action.selector)}).${assertion};`; } case 'assertSnapshot': - return `await Expect(${subject}.${this._asLocator(action.selector)}).ToMatchAriaSnapshotAsync(${quote(action.snapshot)});`; + // Remove [active] attribute from codegen snapshots as it's too transient for stable tests + const cleanedSnapshot = action.snapshot.replace(/\s\[active\]/g, ''); + return `await Expect(${subject}.${this._asLocator(action.selector)}).ToMatchAriaSnapshotAsync(${quote(cleanedSnapshot)});`; } } diff --git a/packages/playwright-core/src/server/codegen/java.ts b/packages/playwright-core/src/server/codegen/java.ts index 42456eb4a9549..a2ddc3513828a 100644 --- a/packages/playwright-core/src/server/codegen/java.ts +++ b/packages/playwright-core/src/server/codegen/java.ts @@ -135,7 +135,9 @@ export class JavaLanguageGenerator implements LanguageGenerator { return `assertThat(${subject}.${this._asLocator(action.selector, inFrameLocator)}).${assertion};`; } case 'assertSnapshot': - return `assertThat(${subject}.${this._asLocator(action.selector, inFrameLocator)}).matchesAriaSnapshot(${quote(action.snapshot)});`; + // Remove [active] attribute from codegen snapshots as it's too transient for stable tests + const cleanedSnapshot = action.snapshot.replace(/\s\[active\]/g, ''); + return `assertThat(${subject}.${this._asLocator(action.selector, inFrameLocator)}).matchesAriaSnapshot(${quote(cleanedSnapshot)});`; } } diff --git a/packages/playwright-core/src/server/codegen/javascript.ts b/packages/playwright-core/src/server/codegen/javascript.ts index 568f0a5113c35..d41219a107190 100644 --- a/packages/playwright-core/src/server/codegen/javascript.ts +++ b/packages/playwright-core/src/server/codegen/javascript.ts @@ -120,7 +120,9 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator { } case 'assertSnapshot': { const commentIfNeeded = this._isTest ? '' : '// '; - return `${commentIfNeeded}await expect(${subject}.${this._asLocator(action.selector)}).toMatchAriaSnapshot(${quoteMultiline(action.snapshot, `${commentIfNeeded} `)});`; + // Remove [active] attribute from codegen snapshots as it's too transient for stable tests + const cleanedSnapshot = action.snapshot.replace(/\s\[active\]/g, ''); + return `${commentIfNeeded}await expect(${subject}.${this._asLocator(action.selector)}).toMatchAriaSnapshot(${quoteMultiline(cleanedSnapshot, `${commentIfNeeded} `)});`; } } } diff --git a/packages/playwright-core/src/server/codegen/python.ts b/packages/playwright-core/src/server/codegen/python.ts index d7b056f083175..365f02b99daa3 100644 --- a/packages/playwright-core/src/server/codegen/python.ts +++ b/packages/playwright-core/src/server/codegen/python.ts @@ -128,7 +128,9 @@ export class PythonLanguageGenerator implements LanguageGenerator { return `expect(${subject}.${this._asLocator(action.selector)}).${assertion};`; } case 'assertSnapshot': - return `expect(${subject}.${this._asLocator(action.selector)}).to_match_aria_snapshot(${quote(action.snapshot)})`; + // Remove [active] attribute from codegen snapshots as it's too transient for stable tests + const cleanedSnapshot = action.snapshot.replace(/\s\[active\]/g, ''); + return `expect(${subject}.${this._asLocator(action.selector)}).to_match_aria_snapshot(${quote(cleanedSnapshot)})`; } }