Files
playwright/tests/page/page-aria-snapshot-ai.spec.ts

934 lines
32 KiB
TypeScript

/**
* 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<Parameters<Page['ariaSnapshot']>[0], 'mode'>): Promise<string> {
return await page.ariaSnapshot({ ...options, mode: 'ai' });
}
it('should generate refs', async ({ page }) => {
await page.setContent(`
<button>One</button>
<button>Two</button>
<button>Three</button>
`);
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(`
<h1>Hello</h1>
<iframe name="foo" src="data:text/html,<h1>World</h1>">
`);
const snapshot1 = await snapshotForAI(page);
expect(snapshot1).toContain('- iframe');
const frameSnapshot = await page.frameLocator(`iframe`).locator('body').ariaSnapshot();
expect(frameSnapshot).toEqual('- heading "World" [level=1]');
});
it('should snapshot a locator inside an iframe', async ({ page }) => {
await page.setContent(`
<h1>Main Page</h1>
<iframe srcdoc="<ul><li>Item 1</li><li>Item 2</li></ul>"></iframe>
`);
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(`
<nav>
<iframe srcdoc="<ul><li><button>Deep</button></li></ul>"></iframe>
</nav>
`);
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 [ref=f1e1]:
- iframe [ref=f1e2]:
- generic [ref=f3e1]: Hi, I'm frame
- iframe [ref=f1e3]:
- generic [ref=f4e1]: Hi, I'm frame
- iframe [ref=e3]:
- generic [ref=f2e1]: 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 re-number refs across navigations but not same-document navigations', async ({ page, server }) => {
server.setRoute('/one.html', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end('<button>One</button>');
});
server.setRoute('/two.html', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end('<button>Two</button>');
});
// The first committed document keeps the base seq, so the main frame has no prefix.
await page.goto(server.PREFIX + '/one.html');
const oneRef = (await snapshotForAI(page)).match(/button "One" \[ref=(e\d+)\]/)![1];
await expect(page.locator(`aria-ref=${oneRef}`)).toHaveText('One');
// Cross-document navigation re-numbers the main frame, so its refs gain a frame prefix.
await page.goto(server.PREFIX + '/two.html');
const twoRef = (await snapshotForAI(page)).match(/button "Two" \[ref=(f\d+e\d+)\]/)![1];
await expect(page.locator(`aria-ref=${twoRef}`)).toHaveText('Two');
// The stale ref from the previous document must not resolve against the new one.
const error = await page.locator(`aria-ref=${oneRef}`).normalize().catch(e => e);
expect(error.message).toContain(`No element matching aria-ref=${oneRef}`);
// Same-document navigation keeps refs intact.
await page.evaluate(() => history.pushState({}, '', '/pushed.html'));
expect(await snapshotForAI(page)).toContain(`button "Two" [ref=${twoRef}]`);
await expect(page.locator(`aria-ref=${twoRef}`)).toHaveText('Two');
});
it('should persist iframe references', async ({ page }) => {
await page.setContent(`
<ul>
<li><iframe srcdoc="<button>button1</button>"></iframe></li>
<li><iframe srcdoc="<button>button2</button>"></iframe></li>
</ul>
`);
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', '<button>button1</button>');
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(`
<button style="pointer-events: none">no-ref</button>
<div style="pointer-events: none">
<button style="pointer-events: auto">with-ref</button>
</div>
<div style="pointer-events: none">
<div style="pointer-events: initial">
<button>with-ref</button>
</div>
</div>
<div style="pointer-events: none">
<div style="pointer-events: auto">
<button>with-ref</button>
</div>
</div>
<div style="pointer-events: auto">
<div style="pointer-events: none">
<button>no-ref</button>
</div>
</div>
`);
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(`
<style>
input {
width: 0;
height: 0;
opacity: 0;
}
</style>
<div>
<label>
<span>
<input type="radio" value="Apple" checked="">
</span>
<span>Apple</span>
</label>
<label>
<span>
<input type="radio" value="Pear">
</span>
<span>Pear</span>
</label>
<label>
<span>
<input type="radio" value="Orange">
</span>
<span>Orange</span>
</label>
</div>
`);
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(`
<div>
<div>
<div>
<button>Button</button>
</div>
</div>
</div>
`);
const snapshot = await snapshotForAI(page);
expect(snapshot).toContainYaml(`
- button \"Button\" [ref=e5]
`);
});
it('should include cursor pointer hint', async ({ page }) => {
await page.setContent(`
<button style="cursor: pointer">Button</button>
`);
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(`
<a style="cursor: pointer" href="about:blank">
Link with a button
<button style="cursor: pointer">Button</button>
</a>
`);
const snapshot = await snapshotForAI(page);
// The link's name is redundant - "Link with a button" prints as text and "Button" as the button -
// so it is dropped even though the node is clickable.
expect(snapshot).toContainYaml(`
- link [ref=e2] [cursor=pointer]:
- /url: about:blank
- text: Link with a button
- button "Button" [ref=e3]
`);
});
it('should omit names that just repeat printed descendant nodes', async ({ page }) => {
await page.setContent(`
<h3><a style="cursor: pointer" href="/issues/1">Clipboard API</a></h3>
`);
const snapshot = await snapshotForAI(page);
expect(snapshot).toContainYaml(`
- heading [level=3] [ref=e2]:
- link "Clipboard API" [ref=e3] [cursor=pointer]:
- /url: /issues/1
`);
});
it('should omit redundant name when a contributing wrapper is collapsed', async ({ page }) => {
// The flex span contributes to the heading's name, but is then removed from the tree as a
// single-child generic wrapper. Its contribution is fully represented by the link, so the
// heading's name is still redundant.
await page.setContent(`
<h3><span style="display: flex"><a style="cursor: pointer" href="/issues/1">Clipboard API</a></span></h3>
`);
const snapshot = await snapshotForAI(page);
expect(snapshot).toContainYaml(`
- heading [level=3] [ref=e2]:
- link "Clipboard API" [ref=e4] [cursor=pointer]:
- /url: /issues/1
`);
});
it('should omit redundant name when a contributor is a skipped leaf generic', async ({ page }) => {
// The outer span is not collapsed (its child is an element, not text), so it becomes a leaf
// generic node that contributes to both names. The link's rendered name covers it, which in turn
// makes the heading's name redundant.
await page.setContent(`
<h3><a style="cursor: pointer" href="/issues/1"><span><span>Clipboard API</span></span></a></h3>
`);
const snapshot = await snapshotForAI(page);
expect(snapshot).toContainYaml(`
- heading [level=3] [ref=e2]:
- link "Clipboard API" [ref=e3] [cursor=pointer]:
- /url: /issues/1
`);
});
it('should keep the name when the contributing wrapper collapses into repeating text', async ({ page }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41985' });
// The outer span is marked as represented while it still wraps the icon. Once the nameless
// image is dropped, the span collapses into a lone text repeating the button's name and removes
// itself, so the button must keep the name derived from it.
await page.setContent(`
<button>
<span>
<span>
<svg focusable="false" tabindex="-1" aria-hidden="true" viewBox="0 0 448 512">
<path d="M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z"/>
</svg>
</span>
<span>Add New Item</span>
</span>
</button>
`);
const snapshot = await snapshotForAI(page);
expect(snapshot).toContainYaml(`
- button "Add New Item" [ref=e2]
`);
});
it('should keep names not derived from printed nodes', async ({ page }) => {
await page.setContent(`
<h3 aria-label="Clipboard API issue"><a style="cursor: pointer" href="/issues/1">Clipboard API</a></h3>
`);
const snapshot = await snapshotForAI(page);
expect(snapshot).toContainYaml(`
- heading "Clipboard API issue" [level=3] [ref=e2]:
- link "Clipboard API" [ref=e3] [cursor=pointer]:
- /url: /issues/1
`);
});
it('should omit images without an accessible name', async ({ page }) => {
await page.setContent(`
<img src="data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA=">
<img alt="A cat" src="data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA=">
<img style="cursor: pointer" src="data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA=">
`);
const snapshot = await snapshotForAI(page);
// A nameless image that cannot be clicked carries no information and is omitted.
expect(snapshot).toContainYaml(`
- generic [active] [ref=e1]:
- img "A cat" [ref=e3]
- img [ref=e4] [cursor=pointer]
`);
expect(snapshot).not.toContain('[ref=e2]');
});
it('should omit a nameless image nested inside a link', async ({ page }) => {
// The decorative image has no name, so it is dropped even though it sits inside a clickable link.
await page.setContent(`
<a style="cursor: pointer" href="/issue/1">Open issue <img src="data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA="></a>
`);
const snapshot = await snapshotForAI(page);
expect(snapshot).toContainYaml(`
- link "Open issue" [ref=e2] [cursor=pointer]:
- /url: /issue/1
`);
expect(snapshot).not.toContain('img');
});
it('should keep icon-only clickable elements', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42013' },
}, async ({ page }) => {
const icon = `<svg viewBox="0 0 1024 1024"><use xlink:href="#icon"></use></svg>`;
await page.setContent(`
<div style="cursor: pointer" aria-haspopup="true"><i>${icon}</i></div>
<img style="cursor: pointer" src="data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA=">
<a style="cursor: pointer" href="/target">${icon}</a>
<div onclick="void 0">${icon}</div>
`);
const snapshot = await snapshotForAI(page);
expect(snapshot).toContainYaml(`
- generic [active] [ref=e1]:
- generic [ref=e2] [cursor=pointer]
- img [ref=e5] [cursor=pointer]
- link [ref=e6] [cursor=pointer]:
- /url: /target
`);
});
it('should omit leaf generic whose text is already in an ancestor name', async ({ page }) => {
// The inner element is block so it survives as its own generic node (an inline single-text span
// would be collapsed into the link instead). It inherits the link's pointer cursor.
await page.setContent(`
<a style="cursor: pointer" href="/issues/15860"><div>[Feature] a dedicated clipboard API</div></a>
`);
const snapshot = await snapshotForAI(page);
// The link keeps its name, and the inner leaf generic that the name was computed from is dropped
// because its text is already shown by the name.
expect(snapshot).toContainYaml(`
- link "[Feature] a dedicated clipboard API" [ref=e2] [cursor=pointer]:
- /url: /issues/15860
`);
expect(snapshot).not.toContain('[ref=e3]');
});
it('should omit name-repeating generic behind a wrapper', async ({ page }) => {
// The leaf generic that repeats the link's name sits inside a nameless wrapper. Its text is
// first inlined into the wrapper, which then faces the link and removes itself.
await page.setContent(`
<a style="cursor: pointer" href="/labels"><span style="display: inline-block"><span style="display: inline-block"><span>P3-collecting-feedback</span></span></span></a>
`);
const snapshot = await snapshotForAI(page);
expect(snapshot).toContainYaml(`
- link "P3-collecting-feedback" [ref=e2] [cursor=pointer]:
- /url: /labels
`);
expect(snapshot.split('P3-collecting-feedback')).toHaveLength(2);
});
it('should resolve refs of distilled-away nodes', async ({ page }) => {
await page.setContent(`
<a style="cursor: pointer" href="/issues/15860"><div>[Feature] a dedicated clipboard API</div></a>
`);
const snapshot = await snapshotForAI(page);
// The inner leaf generic is distilled away, but its ref still resolves to the element.
expect(snapshot).not.toContain('[ref=e3]');
await expect(page.locator('aria-ref=e3')).toHaveText('[Feature] a dedicated clipboard API');
});
it('should not distill snapshots outside of ai mode', async ({ page }) => {
await page.setContent(`
<h3><a href="/issues/1">Clipboard API</a></h3>
`);
// The heading name would be dropped as redundant in ai mode; matching mode keeps it.
await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading "Clipboard API" [level=3]:
- link "Clipboard API":
- /url: /issues/1
`);
});
it('should truncate data url in link', async ({ page }) => {
const base64 = Buffer.from('<p>hello</p>').toString('base64');
await page.setContent(`<a href="data:text/html;base64,${base64}">a link</a>`);
const snapshot = await snapshotForAI(page);
expect(snapshot).toContain('/url: data:text/html;base64,…');
expect(snapshot).not.toContain(base64);
});
it('should gracefully fallback when child frame cant be captured', async ({ page, server }) => {
await page.setContent(`
<p>Test</p>
<iframe src="${server.PREFIX}/redirectloop1.html#depth=100000"></iframe>
`, { 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)
]);
// The snapshot races the reload, which may re-number the main frame, so accept any ref.
expect(snapshot).toMatch(/- generic \[active\] \[ref=(?:f\d+)?e\d+\]: 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(`
<script src="${server.PREFIX}/css"></script>
<p>Hello World</p>
`, { 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(`
<div style="visibility: hidden">
<div style="visibility: visible">
<button>Visible</button>
</div>
<div style="visibility: hidden">
<button style="visibility: visible">Visible</button>
</div>
<div>
<div style="visibility: visible">
<button style="visibility: hidden">Hidden</button>
</div>
<button>Hidden</button>
</div>
</div>
`);
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(`
<button id="btn1">Button 1</button>
<button id="btn2" autofocus>Button 2</button>
<div>Not focusable</div>
`);
// 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(`
<input id="input1" placeholder="First input">
<input id="input2" placeholder="Second input">
`);
// 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(`
<input id="regular-input" placeholder="Regular input">
<iframe src="data:text/html,<input id='iframe-input' placeholder='Input in iframe'>" tabindex="0"></iframe>
`);
// 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(`
<div style="height: 5000px;">Test</div>
<iframe loading="lazy" src="${server.PREFIX}/frame.html"></iframe>
`);
// 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(`
<input id="regular-input" placeholder="Regular input">
<iframe style='cursor: pointer' src="data:text/html,<input id='iframe-input' placeholder='Input in iframe'/>" tabindex="0"></iframe>
`);
// 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 snapshot frameset pages', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41784' } }, async ({ page, server }) => {
server.setRoute('/frameset.html', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`<frameset rows="50%,50%"><frameset cols="50%,50%"><frame src="/frame-one.html"><frame src="/frame-two.html"></frameset><frame src="/frame-three.html"></frameset>`);
});
for (const name of ['one', 'two', 'three']) {
server.setRoute(`/frame-${name}.html`, (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`<button>Button ${name}</button>`);
});
}
await page.goto(server.PREFIX + '/frameset.html');
const snapshot = await snapshotForAI(page, { timeout: 3000 });
expect(snapshot).toContainYaml(`
- generic [active] [ref=e1]:
- generic [ref=e2]:
- iframe [ref=e3]:
- button "Button one" [ref=f1e2]
- iframe [ref=e4]:
- button "Button two" [ref=f2e2]
- iframe [ref=e5]:
- button "Button three" [ref=f3e2]
`);
await expect(page.locator('aria-ref=f2e2')).toHaveText('Button two');
});
it('should snapshot a locator inside a frameset frame', async ({ page, server }) => {
await page.goto(server.PREFIX + '/frames/frameset.html');
const snapshot = await page.frames()[1].locator('body').ariaSnapshot({ mode: 'ai' });
expect(snapshot).toContainYaml(`
- generic [ref=f1e1]: Hi, I'm frame
`);
await expect(page.locator('aria-ref=f1e1')).toHaveText(`Hi, I'm frame`);
const resolved = await page.locator('aria-ref=f1e1').normalize();
expect(resolved.toString()).toBe(`locator('frame').first().contentFrame().locator('body')`);
});
it('should stitch iframes inside a frameset frame', async ({ page, server }) => {
server.setRoute('/frameset-with-iframe.html', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`<frameset><frame src="/frame-with-iframe.html"></frameset>`);
});
server.setRoute('/frame-with-iframe.html', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`<button>In frame</button><iframe srcdoc="<button>In iframe</button>"></iframe>`);
});
await page.goto(server.PREFIX + '/frameset-with-iframe.html');
expect(await snapshotForAI(page)).toContainYaml(`
- iframe [ref=e2]:
- generic [ref=f1e1]:
- button "In frame" [ref=f1e2]
- iframe [ref=f1e3]:
- button "In iframe" [ref=f2e2]
`);
await expect(page.locator('aria-ref=f1e2')).toHaveText('In frame');
await expect(page.locator('aria-ref=f2e2')).toHaveText('In iframe');
const resolved = await page.locator('aria-ref=f2e2').normalize();
expect(resolved.toString()).toBe(`locator('frame').contentFrame().locator('iframe').contentFrame().getByRole('button', { name: 'In iframe' })`);
});
it('should stitch nested frameset documents', async ({ page, server }) => {
server.setRoute('/outer-frameset.html', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`<frameset><frame src="/inner-frameset.html"></frameset>`);
});
server.setRoute('/inner-frameset.html', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`<frameset><frameset><frame src="/leaf.html"></frameset></frameset>`);
});
server.setRoute('/leaf.html', (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`<button>Leaf button</button>`);
});
await page.goto(server.PREFIX + '/outer-frameset.html');
expect(await snapshotForAI(page)).toContainYaml(`
- iframe [ref=e2]:
- iframe [ref=f1e3]:
- button "Leaf button" [ref=f2e2]
`);
await expect(page.locator('aria-ref=f2e2')).toHaveText('Leaf button');
});
it('should collapse inline generic nodes', async ({ page }) => {
await page.setContent(`
<ul>
<li><b>3</b> <abbr>bds</abbr></li>
<li><b>2</b> <abbr>ba</abbr></li>
<li><b>1,200</b> <abbr>sqft</abbr></li>
</ul>
<ul>
<li><div>3</div></li>
<li><div>2</div></li>
<li><div>1,200</div></li>
</ul>`);
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 inline single leaf generic child into parent generic', async ({ page }) => {
// The nameless images are distilled away, so each wrapper is left with a single leaf generic
// child, whose text is inlined into the wrapper - recursively for the second one.
await page.setContent(`
<div><img src="data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA="><div>Status: Open.</div></div>
<div><img src="data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA="><div><img src="data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA="><div>Nested twice.</div></div></div>
`);
const snapshot = await snapshotForAI(page);
expect(snapshot).toContainYaml(`
- generic [active] [ref=e1]:
- generic [ref=e2]: "Status: Open."
- generic [ref=e5]: Nested twice.
`);
});
it('should inline a deeply nested generic', async ({ page }) => {
// Every wrapper contains a nameless image (distilled away) plus a single generic child, so the
// text bubbles up the whole chain - all the way into the body.
const img = `<img src="data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA=">`;
await page.setContent(`
<div>${img}<div>${img}<div>${img}<div>${img}<div>Deeply nested.</div></div></div></div></div>
`);
const snapshot = await snapshotForAI(page);
expect(snapshot).toContainYaml(`
- generic [active] [ref=e1]: Deeply nested.
`);
expect(snapshot).not.toContain('img');
});
it('should not remove generic nodes with title', async ({ page }) => {
await page.setContent(`<div title="Element title">Element content</div>`);
const snapshot = await snapshotForAI(page);
expect(snapshot).toContainYaml(`
- generic "Element title" [ref=e2]
`);
});
it('should limit depth', async ({ page }) => {
await page.setContent(`
<ul>
<li>item1</li>
<a href="about:blank" style="cursor:pointer">link</a>
<li>
<ul id=target>
<li>item2</li>
<li>
<ul>
<li>item3</li>
</ul>
</li>
</ul>
</li>
</ul>
`);
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]
`);
});