diff --git a/README.md b/README.md index e99460db3..1efcead54 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 130.0.6723.31 | ✅ | ✅ | ✅ | -| WebKit 18.0 | ✅ | ✅ | ✅ | -| Firefox 131.0 | ✅ | ✅ | ✅ | +| Chromium 131.0.6778.33 | ✅ | ✅ | ✅ | +| WebKit 18.2 | ✅ | ✅ | ✅ | +| Firefox 132.0 | ✅ | ✅ | ✅ | ## Documentation diff --git a/playwright/_impl/_api_structures.py b/playwright/_impl/_api_structures.py index 904a590a9..3b639486a 100644 --- a/playwright/_impl/_api_structures.py +++ b/playwright/_impl/_api_structures.py @@ -291,3 +291,9 @@ class FrameExpectResult(TypedDict): "treegrid", "treeitem", ] + + +class TracingGroupLocation(TypedDict): + file: str + line: Optional[int] + column: Optional[int] diff --git a/playwright/_impl/_assertions.py b/playwright/_impl/_assertions.py index 13e7ac481..fce405da7 100644 --- a/playwright/_impl/_assertions.py +++ b/playwright/_impl/_assertions.py @@ -783,6 +783,23 @@ async def not_to_have_role(self, role: AriaRole, timeout: float = None) -> None: __tracebackhide__ = True await self._not.to_have_role(role, timeout) + async def to_match_aria_snapshot( + self, expected: str, timeout: float = None + ) -> None: + __tracebackhide__ = True + await self._expect_impl( + "to.match.aria", + FrameExpectOptions(expectedValue=expected, timeout=timeout), + expected, + "Locator expected to match Aria snapshot", + ) + + async def not_to_match_aria_snapshot( + self, expected: str, timeout: float = None + ) -> None: + __tracebackhide__ = True + await self._not.to_match_aria_snapshot(expected, timeout) + class APIResponseAssertions: def __init__( diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index 4645e2415..f415d5900 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -61,7 +61,6 @@ RouteHandlerCallback, TimeoutSettings, URLMatch, - URLMatcher, WebSocketRouteHandlerCallback, async_readfile, async_writefile, @@ -416,7 +415,8 @@ async def route( self._routes.insert( 0, RouteHandler( - URLMatcher(self._options.get("baseURL"), url), + self._options.get("baseURL"), + url, handler, True if self._dispatcher_fiber else False, times, @@ -430,7 +430,7 @@ async def unroute( removed = [] remaining = [] for route in self._routes: - if route.matcher.match != url or (handler and route.handler != handler): + if route.url != url or (handler and route.handler != handler): remaining.append(route) else: removed.append(route) @@ -453,9 +453,7 @@ async def route_web_socket( ) -> None: self._web_socket_routes.insert( 0, - WebSocketRouteHandler( - URLMatcher(self._options.get("baseURL"), url), handler - ), + WebSocketRouteHandler(self._options.get("baseURL"), url, handler), ) await self._update_web_socket_interception_patterns() diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index 1ce813636..d616046e6 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -45,10 +45,10 @@ Literal, MouseButton, URLMatch, - URLMatcher, async_readfile, locals_to_params, monotonic_time, + url_matches, ) from playwright._impl._js_handle import ( JSHandle, @@ -185,18 +185,17 @@ def expect_navigation( to_url = f' to "{url}"' if url else "" waiter.log(f"waiting for navigation{to_url} until '{waitUntil}'") - matcher = ( - URLMatcher(self._page._browser_context._options.get("baseURL"), url) - if url - else None - ) def predicate(event: Any) -> bool: # Any failed navigation results in a rejection. if event.get("error"): return True waiter.log(f' navigated to "{event["url"]}"') - return not matcher or matcher.matches(event["url"]) + return url_matches( + cast("Page", self._page)._browser_context._options.get("baseURL"), + event["url"], + url, + ) waiter.wait_for_event( self._event_emitter, @@ -226,8 +225,9 @@ async def wait_for_url( timeout: float = None, ) -> None: assert self._page - matcher = URLMatcher(self._page._browser_context._options.get("baseURL"), url) - if matcher.matches(self.url): + if url_matches( + self._page._browser_context._options.get("baseURL"), self.url, url + ): await self._wait_for_load_state_impl(state=waitUntil, timeout=timeout) return async with self.expect_navigation( diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py index 027b3e1f5..d0737be07 100644 --- a/playwright/_impl/_helper.py +++ b/playwright/_impl/_helper.py @@ -142,27 +142,26 @@ class FrameNavigatedEvent(TypedDict): Env = Dict[str, Union[str, float, bool]] -class URLMatcher: - def __init__(self, base_url: Union[str, None], match: URLMatch) -> None: - self._callback: Optional[Callable[[str], bool]] = None - self._regex_obj: Optional[Pattern[str]] = None - if isinstance(match, str): - if base_url and not match.startswith("*"): - match = urljoin(base_url, match) - regex = glob_to_regex(match) - self._regex_obj = re.compile(regex) - elif isinstance(match, Pattern): - self._regex_obj = match - else: - self._callback = match - self.match = match - - def matches(self, url: str) -> bool: - if self._callback: - return self._callback(url) - if self._regex_obj: - return cast(bool, self._regex_obj.search(url)) - return False +def url_matches( + base_url: Optional[str], url_string: str, match: Optional[URLMatch] +) -> bool: + if not match: + return True + if isinstance(match, str) and match[0] != "*": + # Allow http(s) baseURL to match ws(s) urls. + if ( + base_url + and re.match(r"^https?://", base_url) + and re.match(r"^wss?://", url_string) + ): + base_url = re.sub(r"^http", "ws", base_url) + if base_url: + match = urljoin(base_url, match) + if isinstance(match, str): + match = glob_to_regex(match) + if isinstance(match, Pattern): + return bool(match.search(url_string)) + return match(url_string) class HarLookupResult(TypedDict, total=False): @@ -271,12 +270,14 @@ def __init__(self, complete: "asyncio.Future", route: "Route") -> None: class RouteHandler: def __init__( self, - matcher: URLMatcher, + base_url: Optional[str], + url: URLMatch, handler: RouteHandlerCallback, is_sync: bool, times: Optional[int] = None, ): - self.matcher = matcher + self._base_url = base_url + self.url = url self.handler = handler self._times = times if times else math.inf self._handled_count = 0 @@ -285,7 +286,7 @@ def __init__( self._active_invocations: Set[RouteHandlerInvocation] = set() def matches(self, request_url: str) -> bool: - return self.matcher.matches(request_url) + return url_matches(self._base_url, request_url, self.url) async def handle(self, route: "Route") -> bool: handler_invocation = RouteHandlerInvocation( @@ -362,13 +363,13 @@ def prepare_interception_patterns( patterns = [] all = False for handler in handlers: - if isinstance(handler.matcher.match, str): - patterns.append({"glob": handler.matcher.match}) - elif isinstance(handler.matcher._regex_obj, re.Pattern): + if isinstance(handler.url, str): + patterns.append({"glob": handler.url}) + elif isinstance(handler.url, re.Pattern): patterns.append( { - "regexSource": handler.matcher._regex_obj.pattern, - "regexFlags": escape_regex_flags(handler.matcher._regex_obj), + "regexSource": handler.url.pattern, + "regexFlags": escape_regex_flags(handler.url), } ) else: diff --git a/playwright/_impl/_locator.py b/playwright/_impl/_locator.py index 521897978..91ea79064 100644 --- a/playwright/_impl/_locator.py +++ b/playwright/_impl/_locator.py @@ -534,6 +534,15 @@ async def screenshot( ), ) + async def aria_snapshot(self, timeout: float = None) -> str: + return await self._frame._channel.send( + "ariaSnapshot", + { + "selector": self._selector, + **locals_to_params(locals()), + }, + ) + async def scroll_into_view_if_needed( self, timeout: float = None, diff --git a/playwright/_impl/_network.py b/playwright/_impl/_network.py index 53f97a46c..97bb049e3 100644 --- a/playwright/_impl/_network.py +++ b/playwright/_impl/_network.py @@ -53,10 +53,11 @@ from playwright._impl._errors import Error from playwright._impl._event_context_manager import EventContextManagerImpl from playwright._impl._helper import ( - URLMatcher, + URLMatch, WebSocketRouteHandlerCallback, async_readfile, locals_to_params, + url_matches, ) from playwright._impl._str_utils import escape_regex_flags from playwright._impl._waiter import Waiter @@ -718,8 +719,14 @@ async def _after_handle(self) -> None: class WebSocketRouteHandler: - def __init__(self, matcher: URLMatcher, handler: WebSocketRouteHandlerCallback): - self.matcher = matcher + def __init__( + self, + base_url: Optional[str], + url: URLMatch, + handler: WebSocketRouteHandlerCallback, + ): + self._base_url = base_url + self.url = url self.handler = handler @staticmethod @@ -729,13 +736,13 @@ def prepare_interception_patterns( patterns = [] all_urls = False for handler in handlers: - if isinstance(handler.matcher.match, str): - patterns.append({"glob": handler.matcher.match}) - elif isinstance(handler.matcher._regex_obj, re.Pattern): + if isinstance(handler.url, str): + patterns.append({"glob": handler.url}) + elif isinstance(handler.url, re.Pattern): patterns.append( { - "regexSource": handler.matcher._regex_obj.pattern, - "regexFlags": escape_regex_flags(handler.matcher._regex_obj), + "regexSource": handler.url.pattern, + "regexFlags": escape_regex_flags(handler.url), } ) else: @@ -746,7 +753,7 @@ def prepare_interception_patterns( return patterns def matches(self, ws_url: str) -> bool: - return self.matcher.matches(ws_url) + return url_matches(self._base_url, ws_url, self.url) async def handle(self, websocket_route: "WebSocketRoute") -> None: coro_or_future = self.handler(websocket_route) diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 15195b28b..62fec2a3f 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -71,7 +71,6 @@ RouteHandlerCallback, TimeoutSettings, URLMatch, - URLMatcher, URLMatchRequest, URLMatchResponse, WebSocketRouteHandlerCallback, @@ -80,6 +79,7 @@ locals_to_params, make_dirs_for_file, serialize_error, + url_matches, ) from playwright._impl._input import Keyboard, Mouse, Touchscreen from playwright._impl._js_handle import ( @@ -380,16 +380,14 @@ def main_frame(self) -> Frame: return self._main_frame def frame(self, name: str = None, url: URLMatch = None) -> Optional[Frame]: - matcher = ( - URLMatcher(self._browser_context._options.get("baseURL"), url) - if url - else None - ) for frame in self._frames: if name and frame.name == name: return frame - if url and matcher and matcher.matches(frame.url): + if url and url_matches( + self._browser_context._options.get("baseURL"), frame.url, url + ): return frame + return None @property @@ -656,7 +654,8 @@ async def route( self._routes.insert( 0, RouteHandler( - URLMatcher(self._browser_context._options.get("baseURL"), url), + self._browser_context._options.get("baseURL"), + url, handler, True if self._dispatcher_fiber else False, times, @@ -670,7 +669,7 @@ async def unroute( removed = [] remaining = [] for route in self._routes: - if route.matcher.match != url or (handler and route.handler != handler): + if route.url != url or (handler and route.handler != handler): remaining.append(route) else: removed.append(route) @@ -699,7 +698,7 @@ async def route_web_socket( self._web_socket_routes.insert( 0, WebSocketRouteHandler( - URLMatcher(self._browser_context._options.get("baseURL"), url), handler + self._browser_context._options.get("baseURL"), url, handler ), ) await self._update_web_socket_interception_patterns() @@ -1235,21 +1234,14 @@ def expect_request( urlOrPredicate: URLMatchRequest, timeout: float = None, ) -> EventContextManagerImpl[Request]: - matcher = ( - None - if callable(urlOrPredicate) - else URLMatcher( - self._browser_context._options.get("baseURL"), urlOrPredicate - ) - ) - predicate = urlOrPredicate if callable(urlOrPredicate) else None - def my_predicate(request: Request) -> bool: - if matcher: - return matcher.matches(request.url) - if predicate: - return predicate(request) - return True + if not callable(urlOrPredicate): + return url_matches( + self._browser_context._options.get("baseURL"), + request.url, + urlOrPredicate, + ) + return urlOrPredicate(request) trimmed_url = trim_url(urlOrPredicate) log_line = f"waiting for request {trimmed_url}" if trimmed_url else None @@ -1274,21 +1266,14 @@ def expect_response( urlOrPredicate: URLMatchResponse, timeout: float = None, ) -> EventContextManagerImpl[Response]: - matcher = ( - None - if callable(urlOrPredicate) - else URLMatcher( - self._browser_context._options.get("baseURL"), urlOrPredicate - ) - ) - predicate = urlOrPredicate if callable(urlOrPredicate) else None - - def my_predicate(response: Response) -> bool: - if matcher: - return matcher.matches(response.url) - if predicate: - return predicate(response) - return True + def my_predicate(request: Response) -> bool: + if not callable(urlOrPredicate): + return url_matches( + self._browser_context._options.get("baseURL"), + request.url, + urlOrPredicate, + ) + return urlOrPredicate(request) trimmed_url = trim_url(urlOrPredicate) log_line = f"waiting for response {trimmed_url}" if trimmed_url else None diff --git a/playwright/_impl/_tracing.py b/playwright/_impl/_tracing.py index d645e41da..a68b53bf7 100644 --- a/playwright/_impl/_tracing.py +++ b/playwright/_impl/_tracing.py @@ -15,6 +15,7 @@ import pathlib from typing import Dict, Optional, Union, cast +from playwright._impl._api_structures import TracingGroupLocation from playwright._impl._artifact import Artifact from playwright._impl._connection import ChannelOwner, from_nullable_channel from playwright._impl._helper import locals_to_params @@ -131,3 +132,9 @@ def _reset_stack_counter(self) -> None: if self._is_tracing: self._is_tracing = False self._connection.set_is_tracing(False) + + async def group(self, name: str, location: TracingGroupLocation = None) -> None: + await self._channel.send("tracingGroup", locals_to_params(locals())) + + async def group_end(self) -> None: + await self._channel.send("tracingGroupEnd") diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index c01b23fc2..e1480f5bf 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -37,6 +37,7 @@ SetCookieParam, SourceLocation, StorageState, + TracingGroupLocation, ViewportSize, ) from playwright._impl._assertions import ( @@ -922,9 +923,8 @@ async def handle(route, request): **Details** - Note that any overrides such as `url` or `headers` only apply to the request being routed. If this request results - in a redirect, overrides will not be applied to the new redirected request. If you want to propagate a header - through redirects, use the combination of `route.fetch()` and `route.fulfill()` instead. + The `headers` option applies to both the routed request and any redirects it initiates. However, `url`, `method`, + and `postData` only apply to the original request and are not carried over to redirected requests. `route.continue_()` will immediately send the request to the network, other matching handlers won't be invoked. Use `route.fallback()` If you want next matching handler in the chain to be invoked. @@ -6923,6 +6923,9 @@ async def set_fixed_time( Makes `Date.now` and `new Date()` return fixed fake time at all times, keeps all the timers running. + Use this method for simple scenarios where you only need to test with a predefined time. For more advanced + scenarios, use `clock.install()` instead. Read docs on [clock emulation](https://playwright.dev/python/docs/clock) to learn more. + **Usage** ```py @@ -6944,7 +6947,8 @@ async def set_system_time( ) -> None: """Clock.set_system_time - Sets current system time but does not trigger any timers. + Sets system time, but does not trigger any timers. Use this to test how the web page reacts to a time shift, for + example switching from summer to winter time, or changing time zones. **Usage** @@ -9294,8 +9298,6 @@ async def emulate_media( # → True await page.evaluate(\"matchMedia('(prefers-color-scheme: light)').matches\") # → False - await page.evaluate(\"matchMedia('(prefers-color-scheme: no-preference)').matches\") - # → False ``` Parameters @@ -9304,8 +9306,9 @@ async def emulate_media( Changes the CSS media type of the page. The only allowed values are `'Screen'`, `'Print'` and `'Null'`. Passing `'Null'` disables CSS media emulation. color_scheme : Union["dark", "light", "no-preference", "null", None] - Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. - Passing `'Null'` disables color scheme emulation. + Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) + media feature, supported values are `'light'` and `'dark'`. Passing `'Null'` disables color scheme emulation. + `'no-preference'` is deprecated. reduced_motion : Union["no-preference", "null", "reduce", None] Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. Passing `null` disables reduced motion emulation. @@ -13804,9 +13807,9 @@ async def new_context( Specifies if viewport supports touch events. Defaults to false. Learn more about [mobile emulation](../emulation.md#devices). color_scheme : Union["dark", "light", "no-preference", "null", None] - Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See - `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to - `'light'`. + Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) + media feature, supported values are `'light'` and `'dark'`. See `page.emulate_media()` for more details. + Passing `'null'` resets emulation to system defaults. Defaults to `'light'`. reduced_motion : Union["no-preference", "null", "reduce", None] Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. See `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to @@ -14029,9 +14032,9 @@ async def new_page( Specifies if viewport supports touch events. Defaults to false. Learn more about [mobile emulation](../emulation.md#devices). color_scheme : Union["dark", "light", "no-preference", "null", None] - Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See - `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to - `'light'`. + Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) + media feature, supported values are `'light'` and `'dark'`. See `page.emulate_media()` for more details. + Passing `'null'` resets emulation to system defaults. Defaults to `'light'`. forced_colors : Union["active", "none", "null", None] Emulates `'forced-colors'` media feature, supported values are `'active'`, `'none'`. See `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to @@ -14341,9 +14344,12 @@ async def launch( resolved relative to the current working directory. Note that Playwright only works with the bundled Chromium, Firefox or WebKit, use at your own risk. channel : Union[str, None] - Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", - "msedge", "msedge-beta", "msedge-dev", "msedge-canary". Read more about using - [Google Chrome and Microsoft Edge](../browsers.md#google-chrome--microsoft-edge). + Browser distribution channel. + + Use "chromium" to [opt in to new headless mode](../browsers.md#opt-in-to-new-headless-mode). + + Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or + "msedge-canary" to use branded [Google Chrome and Microsoft Edge](../browsers.md#google-chrome--microsoft-edge). args : Union[Sequence[str], None] **NOTE** Use custom browser args at your own risk, as some of them may break Playwright functionality. @@ -14496,9 +14502,12 @@ async def launch_persistent_context( user data directory is the **parent** directory of the "Profile Path" seen at `chrome://version`. Pass an empty string to use a temporary directory instead. channel : Union[str, None] - Browser distribution channel. Supported values are "chrome", "chrome-beta", "chrome-dev", "chrome-canary", - "msedge", "msedge-beta", "msedge-dev", "msedge-canary". Read more about using - [Google Chrome and Microsoft Edge](../browsers.md#google-chrome--microsoft-edge). + Browser distribution channel. + + Use "chromium" to [opt in to new headless mode](../browsers.md#opt-in-to-new-headless-mode). + + Use "chrome", "chrome-beta", "chrome-dev", "chrome-canary", "msedge", "msedge-beta", "msedge-dev", or + "msedge-canary" to use branded [Google Chrome and Microsoft Edge](../browsers.md#google-chrome--microsoft-edge). executable_path : Union[pathlib.Path, str, None] Path to a browser executable to run instead of the bundled one. If `executablePath` is a relative path, then it is resolved relative to the current working directory. Note that Playwright only works with the bundled Chromium, @@ -14588,9 +14597,9 @@ async def launch_persistent_context( Specifies if viewport supports touch events. Defaults to false. Learn more about [mobile emulation](../emulation.md#devices). color_scheme : Union["dark", "light", "no-preference", "null", None] - Emulates `'prefers-colors-scheme'` media feature, supported values are `'light'`, `'dark'`, `'no-preference'`. See - `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to - `'light'`. + Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) + media feature, supported values are `'light'` and `'dark'`. See `page.emulate_media()` for more details. + Passing `'null'` resets emulation to system defaults. Defaults to `'light'`. reduced_motion : Union["no-preference", "null", "reduce", None] Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. See `page.emulate_media()` for more details. Passing `'null'` resets emulation to system defaults. Defaults to @@ -15084,6 +15093,48 @@ async def stop( return mapping.from_maybe_impl(await self._impl_obj.stop(path=path)) + async def group( + self, name: str, *, location: typing.Optional[TracingGroupLocation] = None + ) -> None: + """Tracing.group + + **NOTE** Use `test.step` instead when available. + + Creates a new group within the trace, assigning any subsequent API calls to this group, until + `tracing.group_end()` is called. Groups can be nested and will be visible in the trace viewer. + + **Usage** + + ```py + # All actions between group and group_end + # will be shown in the trace viewer as a group. + page.context.tracing.group(\"Open Playwright.dev > API\") + page.goto(\"https://playwright.dev/\") + page.get_by_role(\"link\", name=\"API\").click() + page.context.tracing.group_end() + ``` + + Parameters + ---------- + name : str + Group name shown in the trace viewer. + location : Union[{file: str, line: Union[int, None], column: Union[int, None]}, None] + Specifies a custom location for the group to be shown in the trace viewer. Defaults to the location of the + `tracing.group()` call. + """ + + return mapping.from_maybe_impl( + await self._impl_obj.group(name=name, location=location) + ) + + async def group_end(self) -> None: + """Tracing.group_end + + Closes the last group created by `tracing.group()`. + """ + + return mapping.from_maybe_impl(await self._impl_obj.group_end()) + mapping.register(TracingImpl, Tracing) @@ -17101,6 +17152,61 @@ async def screenshot( ) ) + async def aria_snapshot(self, *, timeout: typing.Optional[float] = None) -> str: + """Locator.aria_snapshot + + Captures the aria snapshot of the given element. Read more about [aria snapshots](https://playwright.dev/python/docs/aria-snapshots) and + `locator_assertions.to_match_aria_snapshot()` for the corresponding assertion. + + **Usage** + + ```py + await page.get_by_role(\"link\").aria_snapshot() + ``` + + **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 +