diff --git a/packages/injected/src/ariaSnapshot.ts b/packages/injected/src/ariaSnapshot.ts index a75a5cd185d6c..a12ff0b896f7e 100644 --- a/packages/injected/src/ariaSnapshot.ts +++ b/packages/injected/src/ariaSnapshot.ts @@ -164,6 +164,8 @@ 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 isActive = element.ownerDocument.activeElement === element; + return { role: 'iframe', name: '', @@ -172,7 +174,8 @@ function toAriaNode(element: Element, options?: { forAI?: boolean, refPrefix?: s props: {}, element, box: box(element), - receivesPointerEvents: true + receivesPointerEvents: true, + active: isActive }; } @@ -192,7 +195,8 @@ function toAriaNode(element: Element, options?: { forAI?: boolean, refPrefix?: s props: {}, element, box: box(element), - receivesPointerEvents + receivesPointerEvents, + active: element.ownerDocument.activeElement === element }; if (roleUtils.kAriaCheckedRoles.includes(role)) @@ -431,6 +435,8 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, options?: { mode?: 'r key += ` [disabled]`; if (ariaNode.expanded) key += ` [expanded]`; + if (ariaNode.active) + key += ` [active]`; if (ariaNode.level) key += ` [level=${ariaNode.level}]`; if (ariaNode.pressed === 'mixed') 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)})`; } } diff --git a/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts b/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts index ffbd50ddef416..2e92b67d1c994 100644 --- a/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts +++ b/packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts @@ -28,6 +28,7 @@ export type AriaProps = { checked?: boolean | 'mixed'; disabled?: boolean; expanded?: boolean; + active?: boolean; level?: number; pressed?: boolean | 'mixed'; selected?: boolean; @@ -443,6 +444,11 @@ export class KeyParser { node.expanded = value === 'true'; return; } + 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') { 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 b9a77596495f6..0d8f90a2cc10e 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]: @@ -128,7 +128,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] @@ -224,8 +224,74 @@ 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] `); }); + +it('should include active element information', async ({ page }) => { + await page.setContent(` + + +