diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md
index 8f0b53e16da7f..ba6dc91353be1 100644
--- a/docs/src/api/class-locator.md
+++ b/docs/src/api/class-locator.md
@@ -150,6 +150,63 @@ var button = page.GetByRole(AriaRole.Button).And(page.GetByTitle("Subscribe"));
Additional locator to match.
+## async method: Locator.ariaSnapshot
+* since: v1.49
+- returns: <[string]>
+
+Captures the aria snapshot of the given element. See [`method: LocatorAssertions.toMatchAriaSnapshot`] for the corresponding assertion.
+
+**Usage**
+
+```js
+await page.getByRole('link').ariaSnapshot();
+```
+
+```java
+page.getByRole(AriaRole.LINK).ariaSnapshot();
+```
+
+```python async
+await page.get_by_role("link").aria_snapshot()
+```
+
+```python sync
+page.get_by_role("link").aria_snapshot()
+```
+
+```csharp
+await page.GetByRole(AriaRole.Link).AriaSnapshotAsync();
+```
+
+**Details**
+
+This method captures the aria snapshot of the given element. The snapshot is a string that represents the state of the element and its children.
+The snapshot can be used to assert the state of the element in the test, or to compare it to state in the future.
+
+The ARIA snapshot is represented using [YAML](https://yaml.org/spec/1.2.2/) markup language:
+* The keys of the objects are the roles and optional accessible names of the elements.
+* The values are either text content or an array of child elements.
+* Generic static text can be represented with the `text` key.
+
+Below is the HTML markup and the respective ARIA snapshot:
+
+```html
+
+ Home
+ About
+
+```
+
+```yml
+- list "Links":
+ - listitem:
+ - link "Home"
+ - listitem:
+ - link "About"
+```
+
+### option: Locator.ariaSnapshot.timeout = %%-input-timeout-js-%%
+* since: v1.49
## async method: Locator.blur
* since: v1.28
diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts
index b6058e0abb825..1b43db8de732e 100644
--- a/packages/playwright-core/src/client/locator.ts
+++ b/packages/playwright-core/src/client/locator.ts
@@ -288,6 +288,11 @@ export class Locator implements api.Locator {
return await this._withElement((h, timeout) => h.screenshot({ ...options, timeout }), options.timeout);
}
+ async ariaSnapshot(options?: TimeoutOptions): Promise {
+ const result = await this._frame._channel.ariaSnapshot({ ...options, selector: this._selector });
+ return result.snapshot;
+ }
+
async scrollIntoViewIfNeeded(options: channels.ElementHandleScrollIntoViewIfNeededOptions = {}) {
return await this._withElement((h, timeout) => h.scrollIntoViewIfNeeded({ ...options, timeout }), options.timeout);
}
diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts
index d5d91b165de75..24ddf0014c0d1 100644
--- a/packages/playwright-core/src/protocol/validator.ts
+++ b/packages/playwright-core/src/protocol/validator.ts
@@ -1424,6 +1424,13 @@ scheme.FrameAddStyleTagParams = tObject({
scheme.FrameAddStyleTagResult = tObject({
element: tChannel(['ElementHandle']),
});
+scheme.FrameAriaSnapshotParams = tObject({
+ selector: tString,
+ timeout: tOptional(tNumber),
+});
+scheme.FrameAriaSnapshotResult = tObject({
+ snapshot: tString,
+});
scheme.FrameBlurParams = tObject({
selector: tString,
strict: tOptional(tBoolean),
diff --git a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts
index d058085cb71b2..2f172df694f34 100644
--- a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts
+++ b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts
@@ -267,4 +267,8 @@ export class FrameDispatcher extends Dispatcher {
+ return { snapshot: await this._frame.ariaSnapshot(metadata, params.selector, params) };
+ }
}
diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts
index 76708245d4ab3..4bb301bffdc56 100644
--- a/packages/playwright-core/src/server/dom.ts
+++ b/packages/playwright-core/src/server/dom.ts
@@ -789,6 +789,10 @@ export class ElementHandle extends js.JSHandle {
return this._page._delegate.getBoundingBox(this);
}
+ async ariaSnapshot(): Promise {
+ return await this.evaluateInUtility(([injected, element]) => injected.ariaSnapshot(element), {});
+ }
+
async screenshot(metadata: CallMetadata, options: ScreenshotOptions & TimeoutOptions = {}): Promise {
const controller = new ProgressController(metadata, this);
return controller.run(
diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts
index 6dc412c235357..597ff359511c3 100644
--- a/packages/playwright-core/src/server/frames.ts
+++ b/packages/playwright-core/src/server/frames.ts
@@ -1405,6 +1405,13 @@ export class Frame extends SdkObject {
});
}
+ async ariaSnapshot(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise {
+ const controller = new ProgressController(metadata, this);
+ return controller.run(async progress => {
+ return await this._retryWithProgressIfNotConnected(progress, selector, true /* strict */, true /* performActionPreChecks */, handle => handle.ariaSnapshot());
+ }, this._page._timeoutSettings.timeout(options));
+ }
+
async expect(metadata: CallMetadata, selector: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }> {
const result = await this._expectImpl(metadata, selector, options);
// Library mode special case for the expect errors which are return values, not exceptions.
diff --git a/packages/playwright-core/src/server/injected/consoleApi.ts b/packages/playwright-core/src/server/injected/consoleApi.ts
index 526694745b8bd..0287240493291 100644
--- a/packages/playwright-core/src/server/injected/consoleApi.ts
+++ b/packages/playwright-core/src/server/injected/consoleApi.ts
@@ -20,7 +20,6 @@ import { escapeForTextSelector } from '../../utils/isomorphic/stringUtils';
import { asLocator } from '../../utils/isomorphic/locatorGenerators';
import type { Language } from '../../utils/isomorphic/locatorGenerators';
import type { InjectedScript } from './injectedScript';
-import { renderedAriaTree } from './ariaSnapshot';
const selectorSymbol = Symbol('selector');
@@ -86,7 +85,7 @@ class ConsoleAPI {
inspect: (selector: string) => this._inspect(selector),
selector: (element: Element) => this._selector(element),
generateLocator: (element: Element, language?: Language) => this._generateLocator(element, language),
- ariaSnapshot: (element?: Element) => renderedAriaTree(element || this._injectedScript.document.body),
+ ariaSnapshot: (element?: Element) => this._injectedScript.ariaSnapshot(element || this._injectedScript.document.body),
resume: () => this._resume(),
...new Locator(injectedScript, ''),
};
diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts
index 66a18848db964..7abc5850bbcaf 100644
--- a/packages/playwright-core/src/server/injected/injectedScript.ts
+++ b/packages/playwright-core/src/server/injected/injectedScript.ts
@@ -206,8 +206,10 @@ export class InjectedScript {
return new Set(result.map(r => r.element));
}
- renderedAriaTree(target: Element): string {
- return renderedAriaTree(target);
+ ariaSnapshot(node: Node): string {
+ if (node.nodeType !== Node.ELEMENT_NODE)
+ throw this.createStacklessError('Can only capture aria snapshot of Element nodes.');
+ return renderedAriaTree(node as Element);
}
querySelectorAll(selector: ParsedSelector, root: Node): Element[] {
diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts
index fd2cdcdb7cdae..c82f55c984487 100644
--- a/packages/playwright-core/src/server/injected/recorder/recorder.ts
+++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts
@@ -714,7 +714,7 @@ class TextAssertionTool implements RecorderTool {
name: 'assertSnapshot',
selector: this._hoverHighlight.selector,
signals: [],
- snapshot: this._recorder.injectedScript.renderedAriaTree(target),
+ snapshot: this._recorder.injectedScript.ariaSnapshot(target),
};
} else {
this._hoverHighlight = this._recorder.injectedScript.generateSelector(target, { testIdAttributeName: this._recorder.state.testIdAttributeName, forTextExpect: true });
diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts
index 7d1d736a13167..092f75b2636bc 100644
--- a/packages/playwright-core/types/types.d.ts
+++ b/packages/playwright-core/types/types.d.ts
@@ -12424,6 +12424,57 @@ export interface Locator {
*/
and(locator: Locator): Locator;
+ /**
+ * Captures the aria snapshot of the given element. See
+ * [expect(locator).toMatchAriaSnapshot(expected[, options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot)
+ * for the corresponding assertion.
+ *
+ * **Usage**
+ *
+ * ```js
+ * await page.getByRole('link').ariaSnapshot();
+ * ```
+ *
+ * **Details**
+ *
+ * This method captures the aria snapshot of the given element. The snapshot is a string that represents the state of
+ * the element and its children. The snapshot can be used to assert the state of the element in the test, or to
+ * compare it to state in the future.
+ *
+ * The ARIA snapshot is represented using [YAML](https://yaml.org/spec/1.2.2/) markup language:
+ * - The keys of the objects are the roles and optional accessible names of the elements.
+ * - The values are either text content or an array of child elements.
+ * - Generic static text can be represented with the `text` key.
+ *
+ * Below is the HTML markup and the respective ARIA snapshot:
+ *
+ * ```html
+ *
+ * Home
+ * About
+ *
+ * ```
+ *
+ * ```yml
+ * - list "Links":
+ * - listitem:
+ * - link "Home"
+ * - listitem:
+ * - link "About"
+ * ```
+ *
+ * @param options
+ */
+ ariaSnapshot(options?: {
+ /**
+ * Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout`
+ * option in the config, or by using the
+ * [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout)
+ * or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods.
+ */
+ timeout?: number;
+ }): Promise;
+
/**
* Calls [blur](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/blur) on the element.
* @param options
diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts
index a0d6a30b9faea..5ecc2f4077e07 100644
--- a/packages/protocol/src/channels.ts
+++ b/packages/protocol/src/channels.ts
@@ -2509,6 +2509,7 @@ export interface FrameChannel extends FrameEventTarget, Channel {
evalOnSelectorAll(params: FrameEvalOnSelectorAllParams, metadata?: CallMetadata): Promise;
addScriptTag(params: FrameAddScriptTagParams, metadata?: CallMetadata): Promise;
addStyleTag(params: FrameAddStyleTagParams, metadata?: CallMetadata): Promise;
+ ariaSnapshot(params: FrameAriaSnapshotParams, metadata?: CallMetadata): Promise;
blur(params: FrameBlurParams, metadata?: CallMetadata): Promise;
check(params: FrameCheckParams, metadata?: CallMetadata): Promise;
click(params: FrameClickParams, metadata?: CallMetadata): Promise;
@@ -2613,6 +2614,16 @@ export type FrameAddStyleTagOptions = {
export type FrameAddStyleTagResult = {
element: ElementHandleChannel,
};
+export type FrameAriaSnapshotParams = {
+ selector: string,
+ timeout?: number,
+};
+export type FrameAriaSnapshotOptions = {
+ timeout?: number,
+};
+export type FrameAriaSnapshotResult = {
+ snapshot: string,
+};
export type FrameBlurParams = {
selector: string,
strict?: boolean,
diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml
index a32ebde3d47cf..c91cecbe6cab7 100644
--- a/packages/protocol/src/protocol.yml
+++ b/packages/protocol/src/protocol.yml
@@ -1875,6 +1875,13 @@ Frame:
flags:
snapshot: true
+ ariaSnapshot:
+ parameters:
+ selector: string
+ timeout: number?
+ returns:
+ snapshot: string
+
blur:
parameters:
selector: string
diff --git a/tests/page/page-aria-snapshot.spec.ts b/tests/page/page-aria-snapshot.spec.ts
new file mode 100644
index 0000000000000..766df6d4855f3
--- /dev/null
+++ b/tests/page/page-aria-snapshot.spec.ts
@@ -0,0 +1,40 @@
+/**
+ * Copyright 2018 Google Inc. All rights reserved.
+ * Modifications 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';
+
+it('should snapshot the check box @smoke', async ({ page }) => {
+ await page.setContent(` `);
+ expect(await page.locator('body').ariaSnapshot()).toBe('- checkbox');
+});
+
+it('should snapshot nested element', async ({ page }) => {
+ await page.setContent(`
+
+
+
`);
+ expect(await page.locator('body').ariaSnapshot()).toBe('- checkbox');
+});
+
+it('should snapshot fragment', async ({ page }) => {
+ await page.setContent(`
+ `);
+ expect(await page.locator('body').ariaSnapshot()).toBe(`- link "Link"\n- link "Link"`);
+});