/** * Copyright (c) Microsoft Corporation. * * 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 as it, expect } from './pageTest'; import { unshift } from '../config/utils'; import type { Page } from 'playwright-core'; async function snapshotForAI(page: Page, options?: Omit[0], 'mode'> & { _track?: string }): Promise { return await page.ariaSnapshot({ ...options, mode: 'ai' }); } it('should generate refs', async ({ page }) => { await page.setContent(` `); const snapshot1 = await snapshotForAI(page); expect(snapshot1).toContainYaml(` - generic [active] [ref=e1]: - button "One" [ref=e2] - button "Two" [ref=e3] - button "Three" [ref=e4] `); await expect(page.locator('aria-ref=e2')).toHaveText('One'); await expect(page.locator('aria-ref=e3')).toHaveText('Two'); await expect(page.locator('aria-ref=e4')).toHaveText('Three'); await page.locator('aria-ref=e3').evaluate((e: HTMLElement) => { e.textContent = 'Not Two'; }); const snapshot2 = await snapshotForAI(page); expect(snapshot2).toContainYaml(` - generic [active] [ref=e1]: - button "One" [ref=e2] - button "Not Two" [ref=e5] - button "Three" [ref=e4] `); }); it('should list iframes', async ({ page }) => { await page.setContent(`

Hello

`); const list = page.frames()[1].locator('ul'); const snapshot = await list.ariaSnapshot({ mode: 'ai' }); expect(snapshot).toContainYaml(` - list [ref=f1e1]: - listitem [ref=f1e2]: Item 1 - listitem [ref=f1e3]: Item 2 `); }); it('should limit depth across iframe boundary', async ({ page }) => { await page.setContent(` `); const snapshot = await snapshotForAI(page, { depth: 3 }); expect(snapshot).toContainYaml(` - navigation [ref=e2]: - iframe [ref=e3]: - list [ref=f1e2]: - listitem [ref=f1e3] `); expect(snapshot).not.toContain('button'); }); 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 [active] [ref=e1]: - iframe [ref=e2]: - generic [active] [ref=f1e1]: - iframe [ref=f1e2]: - generic [ref=f3e2]: Hi, I'm frame - iframe [ref=f1e3]: - generic [ref=f4e2]: Hi, I'm frame - iframe [ref=e3]: - generic [ref=f2e2]: Hi, I'm frame `); const href = await page.locator('aria-ref=e1').evaluate(e => e.ownerDocument.defaultView.location.href); expect(href).toBe(server.PREFIX + '/frames/nested-frames.html'); const href2 = await page.locator('aria-ref=f1e2').evaluate(e => e.ownerDocument.defaultView.location.href); expect(href2).toBe(server.PREFIX + '/frames/two-frames.html'); const href3 = await page.locator('aria-ref=f4e2').evaluate(e => e.ownerDocument.defaultView.location.href); expect(href3).toBe(server.PREFIX + '/frames/frame.html'); { const resolved = await page.locator('aria-ref=e1').normalize(); expect(resolved.toString()).toBe(`locator('body')`); } { const resolved = await page.locator('aria-ref=f4e2').normalize(); expect(resolved.toString()).toBe(`locator('iframe[name="2frames"]').contentFrame().locator('iframe[name="dos"]').contentFrame().getByText('Hi, I\\'m frame')`); } { // Should tolerate .describe(). const resolved = await page.locator('aria-ref=f3e2').describe('foo bar').normalize(); expect(resolved.toString()).toBe(`locator('iframe[name=\"2frames\"]').contentFrame().locator('iframe[name=\"uno\"]').contentFrame().getByText('Hi, I\\'m frame')`); } { const error = await page.locator('aria-ref=e1000').normalize().catch(e => e); expect(error.message).toContain(`No element matching aria-ref=e1000`); } }); it('should persist iframe references', async ({ page }) => { await page.setContent(` `); expect(await snapshotForAI(page)).toContainYaml(` - list [ref=e2]: - listitem [ref=e3]: - iframe [ref=e4]: - button "button1" [ref=f1e2] - listitem [ref=e5]: - iframe [ref=e6]: - button "button2" [ref=f2e2] `); await page.evaluate(() => document.querySelector('iframe').remove()); expect(await snapshotForAI(page)).toContainYaml(` - list [ref=e2]: - listitem [ref=e3] - listitem [ref=e5]: - iframe [ref=e6]: - button "button2" [ref=f2e2] `); await expect(page.locator('aria-ref=f2e2')).toHaveText('button2'); await page.evaluate(() => { const frame = document.createElement('iframe'); frame.setAttribute('srcdoc', ''); document.querySelector('li').appendChild(frame); }); expect(await snapshotForAI(page)).toContainYaml(` - list [ref=e2]: - listitem [ref=e3]: - iframe [ref=e7]: - button "button1" [ref=f3e2] - listitem [ref=e5]: - iframe [ref=e6]: - button "button2" [ref=f2e2] `); await expect(page.locator('aria-ref=f3e2')).toHaveText('button1'); await expect(page.locator('aria-ref=f2e2')).toHaveText('button2'); }); it('should not generate refs for elements with pointer-events:none', async ({ page }) => { await page.setContent(`
`); const snapshot = await snapshotForAI(page); expect(snapshot).toContainYaml(` - generic [active] [ref=e1]: - button "no-ref" - button "with-ref" [ref=e2] - button "with-ref" [ref=e4] - button "with-ref" [ref=e6] - generic [ref=e7]: - generic: - button "no-ref" `); }); it('emit generic roles for nodes w/o roles', async ({ page }) => { await page.setContent(`
`); const snapshot = await snapshotForAI(page); expect(snapshot).toContainYaml(` - generic [ref=e2]: - generic [ref=e3]: - generic [ref=e4]: - radio "Apple" [checked] - text: Apple - generic [ref=e5]: - generic [ref=e6]: - radio "Pear" - text: Pear - generic [ref=e7]: - generic [ref=e8]: - radio "Orange" - text: Orange `); }); it('should collapse generic nodes', async ({ page }) => { await page.setContent(`
`); const snapshot = await snapshotForAI(page); expect(snapshot).toContainYaml(` - button \"Button\" [ref=e5] `); }); it('should include cursor pointer hint', async ({ page }) => { await page.setContent(` `); const snapshot = await snapshotForAI(page); expect(snapshot).toContainYaml(` - button \"Button\" [ref=e2] [cursor=pointer] `); }); it('should not nest cursor pointer hints', async ({ page }) => { await page.setContent(` Link with a button `); const snapshot = await snapshotForAI(page); expect(snapshot).toContainYaml(` - link \"Link with a button Button\" [ref=e2] [cursor=pointer]: - /url: about:blank - text: Link with a button - button "Button" [ref=e3] `); }); it('should gracefully fallback when child frame cant be captured', async ({ page, server }) => { await page.setContent(`

Test

`, { waitUntil: 'domcontentloaded' }); const snapshot = await snapshotForAI(page); expect(snapshot).toContainYaml(` - generic [active] [ref=e1]: - paragraph [ref=e2]: Test - iframe [ref=e3] `); }); it('should auto-wait for navigation', async ({ page, server }) => { await page.goto(server.PREFIX + '/frames/frame.html'); const [, snapshot] = await Promise.all([ page.evaluate(() => window.location.reload()), snapshotForAI(page) ]); expect(snapshot).toContainYaml(` - generic [ref=e2]: Hi, I'm frame `); }); it('should auto-wait for blocking CSS', async ({ page, server }) => { server.setRoute('/css', (req, res) => { res.setHeader('Content-Type', 'text/css'); setTimeout(() => res.end(`body { monospace }`), 1000); }); await page.setContent(`

Hello World

`, { waitUntil: 'commit' }); expect(await snapshotForAI(page)).toContainYaml('Hello World'); }); it('should show visible children of hidden elements', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/36296' } }, async ({ page }) => { await page.setContent(`
`); expect(await snapshotForAI(page)).toEqual(unshift(` - generic [active] [ref=e1]: - button "Visible" [ref=e3] - button "Visible" [ref=e4] `)); }); it('should include active element information', async ({ page }) => { await page.setContent(`
Not focusable
`); // Wait for autofocus to take effect await page.waitForFunction(() => document.activeElement?.id === 'btn2'); const snapshot = await snapshotForAI(page); 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 }) => { await page.setContent(` `); // Initially there shouldn't be an active element on the inputs const initialSnapshot = await snapshotForAI(page); 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(); // After focus, the second input should be active const afterFocusSnapshot = await snapshotForAI(page); expect(afterFocusSnapshot).toContainYaml(` - generic [ref=e1]: - textbox "First input" [ref=e2] - 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); // The iframe should be marked as active when it contains a focused element expect(inputInIframeFocusedSnapshot).toContainYaml(` - generic [ref=e1]: - textbox "Regular input" [ref=e2] - iframe [active] [ref=e3]: - textbox "Input in iframe" [active] [ref=f1e2] `); }); it('return empty snapshot when iframe is not loaded', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/pull/36710' } }, async ({ page, server }) => { await page.setContent(`
Test
`); // Wait for the iframe to load await page.waitForSelector('iframe'); // Get the snapshot of the page const snapshot = await snapshotForAI(page, { timeout: 3000 }); // The iframe should be present but empty expect(snapshot).toContainYaml(` - generic [active] [ref=e1]: - generic [ref=e2]: Test - iframe [ref=e3] `); }); it('should support many properties on iframes', async ({ page }) => { await page.setContent(` `); // Test 1: Focus the input inside the iframe await page.frameLocator('iframe').locator('#iframe-input').focus(); const inputInIframeFocusedSnapshot = await snapshotForAI(page); expect(inputInIframeFocusedSnapshot).toContainYaml(` - generic [ref=e1]: - textbox "Regular input" [ref=e2] - iframe [active] [ref=e3] [cursor=pointer]: - textbox "Input in iframe" [active] [ref=f1e2] `); }); it('should collapse inline generic nodes', async ({ page }) => { await page.setContent(` `); const snapshot1 = await snapshotForAI(page); expect(snapshot1).toContainYaml(` - generic [active] [ref=e1]: - list [ref=e2]: - listitem [ref=e3]: 3 bds - listitem [ref=e4]: 2 ba - listitem [ref=e5]: 1,200 sqft - list [ref=e6]: - listitem [ref=e7]: - generic [ref=e8]: "3" - listitem [ref=e9]: - generic [ref=e10]: "2" - listitem [ref=e11]: - generic [ref=e12]: 1,200 `); }); it('should not remove generic nodes with title', async ({ page }) => { await page.setContent(`
Element content
`); const snapshot = await snapshotForAI(page); expect(snapshot).toContainYaml(` - generic "Element title" [ref=e2] `); }); it('should create incremental snapshots on multiple tracks', async ({ page }) => { await page.setContent(``); expect(await snapshotForAI(page, { _track: 'first' })).toContainYaml(` - list [ref=e2]: - listitem [ref=e3]: - button "a button" [ref=e4] - listitem [ref=e5]: a span `); expect(await snapshotForAI(page, { _track: 'second' })).toContainYaml(` - list [ref=e2]: - listitem [ref=e3]: - button "a button" [ref=e4] - listitem [ref=e5]: a span `); expect(await snapshotForAI(page, { _track: 'first' })).toContainYaml(` `); await page.evaluate(() => { document.querySelector('span').textContent = 'changed span'; document.getElementById('hidden-li').style.display = 'inline'; }); expect(await snapshotForAI(page, { _track: 'first' })).toContainYaml(` - list [ref=e2]: - ref=e3 [unchanged] - listitem [ref=e5]: changed span - listitem [ref=e6]: some text `); await page.evaluate(() => { document.querySelector('span').textContent = 'a span'; document.getElementById('hidden-li').style.display = 'none'; }); expect(await snapshotForAI(page, { _track: 'first' })).toContainYaml(` - list [ref=e2]: - ref=e3 [unchanged] - listitem [ref=e5]: a span `); expect(await snapshotForAI(page, { _track: 'second' })).toContainYaml(` `); }); it('should create incremental snapshot for attribute change', async ({ page }) => { await page.setContent(``); await page.evaluate(() => document.querySelector('button').focus()); expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - button "a button" [active] [ref=e2] `); await page.evaluate(() => document.querySelector('button').blur()); expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - button "a button" [ref=e2] `); }); it('should create incremental snapshot for child removal', async ({ page }) => { await page.setContent(`
  • some text
  • `); expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - listitem [ref=e2]: - button "a button" [ref=e3] - text: some text `); await page.evaluate(() => document.querySelector('span').remove()); expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - listitem [ref=e2]: - ref=e3 [unchanged] `); }); it('should create incremental snapshot for child addition', async ({ page }) => { await page.setContent(`
  • some text
  • `); expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - listitem [ref=e2]: - button "a button" [ref=e3] `); await page.evaluate(() => document.querySelector('span').style.display = 'inline'); expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - listitem [ref=e2]: - ref=e3 [unchanged] - text: some text `); }); it('should create incremental snapshot for prop change', async ({ page }) => { await page.setContent(`a link`); expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - link "a link" [ref=e2] [cursor=pointer]: - /url: about:blank `); await page.evaluate(() => document.querySelector('a').setAttribute('href', 'https://playwright.dev')); expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - link "a link" [ref=e2] [cursor=pointer]: - /url: https://playwright.dev `); }); it('should create incremental snapshot for cursor change', async ({ page }) => { await page.setContent(`a link`); expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - link "a link" [ref=e2] [cursor=pointer]: - /url: about:blank `); await page.evaluate(() => document.querySelector('a').style.cursor = 'default'); expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - link "a link" [ref=e2]: - /url: about:blank `); }); it('should create incremental snapshot for name change', async ({ page }) => { await page.setContent(``); expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - button "a button" [ref=e2] `); await page.evaluate(() => document.querySelector('span').textContent = 'new button'); expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - button "new button" [ref=e3] `); }); it('should create incremental snapshot for text change', async ({ page }) => { await page.setContent(`
  • an item
  • `); expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - listitem [ref=e2]: an item `); await page.evaluate(() => document.querySelector('span').textContent = 'new text'); expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - listitem [ref=e2]: new text `); }); it('should produce incremental snapshot for iframes', async ({ page }) => { await page.setContent(` `); expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - iframe [ref=e2]: - listitem [ref=f1e2]: - button "a button" [ref=f1e3] `); await page.frames()[1].evaluate(() => { document.querySelector('span').style.display = 'block'; document.querySelector('iframe').style.display = 'block'; }); expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - listitem [ref=f1e2]: - generic [ref=f1e4]: outer text - ref=f1e3 [unchanged] - iframe [ref=f1e5] - iframe [ref=f1e5]: - listitem [ref=f2e2]: inner text `); }); it('should create multiple chunks in incremental snapshot', async ({ page }) => { await page.setContent(`
    • item1
    • item2
    • item3
      • to be removed
      • one more
    `); expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - list [ref=e2]: - listitem [ref=e3]: item1 - listitem [ref=e4]: item2 - listitem [ref=e5]: - group [ref=e6]: item3 - list [ref=e7]: - listitem [ref=e8]: to be removed - listitem [ref=e9]: one more `); await page.evaluate(() => { const spans = document.querySelectorAll('span'); spans[0].textContent = 'new item1'; spans[2].textContent = 'new item3'; const button = document.createElement('button'); button.textContent = 'button'; spans[2].parentElement.appendChild(button); document.querySelector('#to-remove').remove(); }); expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - listitem [ref=e3]: new item1 - group [ref=e6]: - text: new item3 - button "button" [ref=e10] - list [ref=e7]: - ref=e9 [unchanged] `); }); it('should create incremental snapshot for children swap', async ({ page }) => { await page.setContent(`
    • item 1
    • item 2
    `); expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - list [ref=e2]: - listitem [ref=e3]: item 1 - listitem [ref=e4]: item 2 `); await page.evaluate(() => document.querySelector('ul').appendChild(document.querySelector('li'))); expect(await snapshotForAI(page, { _track: 'track' })).toContainYaml(` - list [ref=e2]: - ref=e4 [unchanged] - ref=e3 [unchanged] `); }); it('should limit depth', async ({ page }) => { await page.setContent(`
    • item1
    • link
      • item2
        • item3
    `); const snapshot1 = await snapshotForAI(page, { depth: 1 }); expect(snapshot1).toContainYaml(` - list [ref=e2]: - listitem [ref=e3]: item1 - link "link" [ref=e4] [cursor=pointer]: - /url: about:blank - listitem [ref=e5] `); const snapshot2 = await snapshotForAI(page, { depth: 3 }); expect(snapshot2).toContainYaml(` - list [ref=e2]: - listitem [ref=e3]: item1 - link "link" [ref=e4] [cursor=pointer]: - /url: about:blank - listitem [ref=e5]: - list [ref=e6]: - listitem [ref=e7]: item2 - listitem [ref=e8] `); const snapshot3 = await snapshotForAI(page, { depth: 100 }); expect(snapshot3).toContainYaml(` - list [ref=e2]: - listitem [ref=e3]: item1 - link "link" [ref=e4] [cursor=pointer]: - /url: about:blank - listitem [ref=e5]: - list [ref=e6]: - listitem [ref=e7]: item2 - listitem [ref=e8]: - list [ref=e9]: - listitem [ref=e10]: item3 `); const snapshot4 = await page.locator('#target').ariaSnapshot({ mode: 'ai', depth: 1 }); expect(snapshot4).toContainYaml(` - list [ref=e6]: - listitem [ref=e7]: item2 - listitem [ref=e8] `); });