diff --git a/.claude/skills/playwright-roll/SKILL.md b/.claude/skills/playwright-roll/SKILL.md index f3fe9e675..597f5d003 100644 --- a/.claude/skills/playwright-roll/SKILL.md +++ b/.claude/skills/playwright-roll/SKILL.md @@ -241,6 +241,8 @@ The script, for `sync` and then `async`: 1. runs `node scripts/codegen/generate_api.js --flavor ` (reads the docs from `$PW_SRC_DIR`) straight into `playwright/_api/_generated.py`, 2. invokes `pre-commit run --files` on the generated file. +It then runs `python scripts/codegen/generate_protocol.py`, which reads `packages/protocol/spec/*.yml` from `$PW_SRC_DIR` and writes the command parameter tables to `playwright/_impl/_protocol.py`. + Then run `mypy playwright` and work through the errors in `_generated.py` — each one is a call into `_impl/` that the implementation does not support yet. To see exactly what a regeneration changed in the public API, compare against the committed file: @@ -272,6 +274,7 @@ Failure modes and fixes: | A kwarg in `_impl/` that the docs don't mention (e.g. legacy name from a doc rename) | Nothing fails: the generator simply never passes it. | If the param is positional with no default, just rename it in `_impl/`. Check `packages/protocol/spec/*.yml` and the server dispatcher — if the wire field renamed too, also update the channel-send dict key. Only use `extraOptions` for genuinely hidden internal kwargs (`default_browser_type`). | | `AttributeError: 'X' object has no attribute '_loop'` (or `_dispatcher_fiber`) at first use of a new wrapper class | The non-ChannelOwner wrapper isn't initializing the fields the generated `AsyncBase`/`SyncBase` reads. | In the wrapper's `__init__`, set `self._loop = parent._loop` and `self._dispatcher_fiber = parent._dispatcher_fiber`. | | `'NoneType' object has no attribute 'get'` after `send_return_as_dict` | Method's protocol response carries no fields and `send_return_as_dict` returned `None`. | `(result or {}).get(...)`. | +| A test fails at teardown with `Protocol violations: X.y: params.z: unknown` (or `missing`, or `expected …`) | The client sends a message that the protocol spec does not allow. The driver would drop an unknown parameter without an error. | Fix the send in `_impl/`: rename the key after a wire rename, or remove a client-side option before the send. | | Frame/buffer payload arrives as a `str` instead of `bytes` | Protocol `binary` fields cross the wire as base64. | `base64.b64decode(value)` in the impl before exposing. | After the script settles, run `pre-commit run --all-files` once more to confirm everything is idle. @@ -323,7 +326,7 @@ All keys use docs names: `Class.member` or `Class.member.arg`, with the upstream - **A cluster of rules on the same class is a smell.** If you're about to add five entries for `Foo.*`, you're almost certainly looking at a class that needs to be implemented. Implement the whole thing once and the entries disappear. - **Watch for revert pairs in the same range.** 1.59 added and reverted `Browser.isRemote` (#39613 / #39620) inside the same release. Walking chronologically lets you skip the add when you see the revert later. - **Watch for rename-revert pairs.** 1.59 had `Locator.normalize` → `Locator.toCode` (#39648) → `Locator.normalize` (#39754). Final state wins; only port the last. -- **Doc renames almost always include a wire-protocol rename.** Whenever you see `### param: X.y.oldName` → `### param: X.y.newName` in a doc commit, also `git -C "$PW_SRC_DIR" show -- packages/protocol/spec/` and the corresponding `*Dispatcher.ts` file. If the wire field changed too, the channel-send dict key in `_impl/` must change. Suppressing the doc-side mismatch is hiding a real bug — the previous Python code is silently sending an unknown field that the new server ignores. +- **Doc renames almost always include a wire-protocol rename.** Whenever you see `### param: X.y.oldName` → `### param: X.y.newName` in a doc commit, also `git -C "$PW_SRC_DIR" show -- packages/protocol/spec/` and the corresponding `*Dispatcher.ts` file. If the wire field changed too, the channel-send dict key in `_impl/` must change. Suppressing the doc-side mismatch is hiding a real bug — the previous Python code is silently sending an unknown field that the new server ignores. The test suite reports such a field as a protocol violation, but only in tests that send it. - **TypedDicts beat `Dict[str, X]` for any structured return.** When the docs describe a return as `[Object]` with named fields, name the struct for the generator, which writes its `TypedDict` into `_api_structures.py`, and re-export it from both public `__init__.py` files. Zero runtime cost (it's still a `dict`), and the docstrings expand its fields from the docs. - **Positional renames are free.** A param with no default before any `*` separator is positional-or-keyword in Python, but realistic call sites pass it positionally. Renaming such a param doesn't break callers. - **The "Backport changes" GitHub issue can be misleading.** In the 1.59 roll its checkboxes were all marked `[x]` with annotations like "✅ IMPLEMENTED", but several of those features had not actually been merged into the Python port. Trust the `docs/src/api/` walk over the issue. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2b39d44b5..7de215962 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: rev: v1.17.0 hooks: - id: mypy - additional_dependencies: [types-pyOpenSSL==24.1.0.20240722] + additional_dependencies: [types-pyOpenSSL==24.1.0.20240722, types-PyYAML==6.0.12.20260906] - repo: https://github.com/pycqa/flake8 rev: 7.3.0 hooks: diff --git a/CLAUDE.md b/CLAUDE.md index d72b6f1a1..f5373d463 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,6 +12,7 @@ Python bindings for [Playwright](https://playwright.dev). The Python client talk - `playwright/async_api/_generated.py`, `playwright/sync_api/_generated.py`, and the public TypedDicts in `playwright/_impl/_api_structures.py` — **auto-generated** from the upstream API docs. Never edit by hand; rerun `./scripts/update_api.sh` after a roll or after changing `scripts/codegen/`. - `scripts/codegen/generate_api.js` — the generator. A Node.js script that loads the upstream doc parser (`utils/doclint/api_parser.js`) from `$PW_SRC_DIR`, filters the docs for Python, and writes the whole public API: signatures, types, docstrings, event overloads, the calls into `_impl/`, and the TypedDicts. It does not read `_impl/`. - `scripts/codegen/python_rules.js` — the Python facts that the docs do not contain (positional options, struct `total=`, hidden members and options). Policy: the shipped Python API wins; each entry is a candidate for an upstream docs fix, and the generator fails on entries that no longer match anything. Every entry needs a one-line rationale comment above it. +- `playwright/_impl/_protocol.py` — **auto-generated** by `scripts/codegen/generate_protocol.py` (run by `./scripts/update_api.sh`) from the upstream protocol spec: the parameters of every command. With `PW_CHECK_PROTOCOL=1`, `playwright/_impl/_protocol_check.py` checks each message that the client sends against it. The test suite sets this, and fails a test that sends an unknown command, an unknown or missing parameter, or a value of the wrong type. - `scripts/codegen/compare_api.py` — compares two generated files (signatures, annotations, call bodies, docstrings) and lists every difference. Use it to check that a generator change does not change the API. - `tests/async/`, `tests/sync/` — pytest suites. Most new tests are added to the async file with a sync mirror. - `DRIVER_VERSION` — the single source of truth for which Playwright release the driver is assembled from (one line, the `playwright-core` npm version, e.g. `1.61.0`, no `v` prefix). Read by `setup.py`, `scripts/build_driver.py`, and CI. The wheel build downloads `playwright-core` at this version from npm plus the matching Node.js binary and assembles the per-platform bundles — no source build. The version is baked into the staged bundle filenames (`driver/playwright--.zip`), so it doubles as the build cache key. diff --git a/local-requirements.txt b/local-requirements.txt index 33681db45..8621cac34 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -11,6 +11,8 @@ pytest==9.0.3 pytest-asyncio==1.4.0 pytest-rerunfailures==16.7 pytest-timeout==2.4.0 +PyYAML==6.0.3 service_identity==24.2.0 twisted==25.5.0 types-pyOpenSSL==24.1.0.20240722 +types-PyYAML==6.0.12.20260906 diff --git a/playwright/_impl/_browser.py b/playwright/_impl/_browser.py index 227d3ccfc..605e637e0 100644 --- a/playwright/_impl/_browser.py +++ b/playwright/_impl/_browser.py @@ -275,11 +275,12 @@ async def start_tracing( categories: Sequence[str] = None, ) -> None: params = locals_to_params(locals()) + # The client writes the trace to `path` in stop_tracing(). + params.pop("path", None) if page: params["page"] = page._channel if path: self._cr_tracing_path = str(path) - params["path"] = str(path) await self._channel.send("startTracing", None, params) async def stop_tracing(self) -> bytes: diff --git a/playwright/_impl/_connection.py b/playwright/_impl/_connection.py index 9480823b5..cf116471a 100644 --- a/playwright/_impl/_connection.py +++ b/playwright/_impl/_connection.py @@ -15,7 +15,6 @@ import asyncio import collections.abc import contextvars -import datetime import inspect import random import string @@ -49,6 +48,7 @@ create_task_and_ignore_exception, parse_error, ) +from playwright._impl._protocol_check import check_message, is_enabled from playwright._impl._transport import Transport if TYPE_CHECKING: @@ -326,6 +326,7 @@ def __init__( self._local_utils: Optional["LocalUtils"] = local_utils self._tracing_count = 0 self._closed_error: Optional[Exception] = None + self._check_protocol = is_enabled() @property def local_utils(self) -> "LocalUtils": @@ -427,14 +428,12 @@ def _send_message_to_server( if frames else None ) - metadata = { - "wallTime": int(datetime.datetime.now().timestamp() * 1000), - "apiName": stack_trace_information["apiName"], + metadata: Dict[str, Any] = { "internal": not stack_trace_information["apiName"], "timeout": timeout, } if location: - metadata["location"] = location # type: ignore + metadata["location"] = location title = stack_trace_information["title"] if title: metadata["title"] = title @@ -445,6 +444,14 @@ def _send_message_to_server( "params": self._replace_channels_with_guids(params), "metadata": metadata, } + if self._check_protocol: + check_message( + object._type, + method, + message["params"], + metadata, + self._type_of_guid, + ) if self._tracing_count > 0 and frames and object._guid != "localUtils": self.local_utils.add_stack_to_tracing_no_reply(id, frames) @@ -453,6 +460,10 @@ def _send_message_to_server( return callback + def _type_of_guid(self, guid: str) -> Optional[str]: + object = self._objects.get(guid) + return object._type if object else None + async def _abort( self, object: ChannelOwner, callback: ProtocolCallback, reason: str ) -> None: diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index 1bf4a8ae9..ad7f489d4 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -650,7 +650,9 @@ async def _fill( force: bool = None, title: str = None, ) -> None: - await self._channel.send("fill", self._timeout, locals_to_params(locals())) + params = locals_to_params(locals()) + params.pop("title", None) + await self._channel.send("fill", self._timeout, params, title=title) def locator( self, @@ -938,6 +940,7 @@ async def wait_for_function( if isinstance(polling, str) and polling != "raf": raise Error(f"Unknown polling option: {polling}") params = locals_to_params(locals()) + params.pop("polling", None) params["arg"] = serialize_argument(arg) if polling is not None and polling != "raf": params["pollingInterval"] = polling diff --git a/playwright/_impl/_glob.py b/playwright/_impl/_glob.py deleted file mode 100644 index b38826996..000000000 --- a/playwright/_impl/_glob.py +++ /dev/null @@ -1,65 +0,0 @@ -# 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. - -# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping -escaped_chars = {"$", "^", "+", ".", "*", "(", ")", "|", "\\", "?", "{", "}", "[", "]"} - - -def glob_to_regex_pattern(glob: str) -> str: - tokens = ["^"] - in_group = False - - i = 0 - while i < len(glob): - c = glob[i] - if c == "\\" and i + 1 < len(glob): - char = glob[i + 1] - tokens.append("\\" + char if char in escaped_chars else char) - i += 1 - elif c == "*": - char_before = glob[i - 1] if i > 0 else None - star_count = 1 - while i + 1 < len(glob) and glob[i + 1] == "*": - star_count += 1 - i += 1 - if star_count > 1: - char_after = glob[i + 1] if i + 1 < len(glob) else None - if char_after == "/": - if char_before == "/": - tokens.append("((.+/)|)") - else: - tokens.append("(.*/)") - i += 1 - else: - tokens.append("(.*)") - else: - tokens.append("([^/]*)") - else: - if c == "{": - in_group = True - tokens.append("(") - elif c == "}": - in_group = False - tokens.append(")") - elif c == ",": - if in_group: - tokens.append("|") - else: - tokens.append("\\" + c) - else: - tokens.append("\\" + c if c in escaped_chars else c) - i += 1 - - tokens.append("$") - return "".join(tokens) diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py index 6dc08f21e..1bdb5a591 100644 --- a/playwright/_impl/_helper.py +++ b/playwright/_impl/_helper.py @@ -31,14 +31,12 @@ Optional, Pattern, Set, - Tuple, TypedDict, TypeVar, Union, cast, overload, ) -from urllib.parse import ParseResult, urljoin, urlparse, urlunparse from playwright._impl._errors import ( Error, @@ -47,16 +45,15 @@ is_target_closed_error, rewrite_error, ) -from playwright._impl._glob import glob_to_regex_pattern from playwright._impl._greenlets import RouteGreenlet from playwright._impl._internal_structures import NameValue from playwright._impl._str_utils import escape_regex_flags +from playwright._impl._url_match import URLMatch, url_matches if TYPE_CHECKING: # pragma: no cover from playwright._impl._internal_structures import HeadersArray from playwright._impl._network import Request, Response, Route, WebSocketRoute -URLMatch = Union[str, Pattern[str], Callable[[str], bool]] URLMatchRequest = Union[ str, Pattern[str], Callable[["Request"], Union[bool, Awaitable[bool]]] ] @@ -153,133 +150,6 @@ class FrameNavigatedEvent(TypedDict): Env = Dict[str, Union[str, float, bool]] -def url_matches( - base_url: Optional[str], - url_string: str, - match: Optional[URLMatch], - websocket_url: bool = None, -) -> bool: - if not match: - return True - if isinstance(match, str): - match = re.compile( - resolve_glob_to_regex_pattern(base_url, match, websocket_url) - ) - if isinstance(match, Pattern): - return bool(match.search(url_string)) - return match(url_string) - - -def resolve_glob_to_regex_pattern( - base_url: Optional[str], glob: str, websocket_url: bool = None -) -> str: - if websocket_url: - base_url = to_websocket_base_url(base_url) - glob = resolve_glob_base(base_url, glob) - return glob_to_regex_pattern(glob) - - -def to_websocket_base_url(base_url: Optional[str]) -> Optional[str]: - if base_url is not None and re.match(r"^https?://", base_url): - base_url = re.sub(r"^http", "ws", base_url) - return base_url - - -def resolve_glob_base(base_url: Optional[str], match: str) -> str: - if match[0] == "*": - return match - - token_map: Dict[str, str] = {} - - def map_token(original: str, replacement: str) -> str: - if len(original) == 0: - return "" - token_map[replacement] = original - return replacement - - # Escaped `\\?` behaves the same as `?` in our glob patterns. - match = match.replace(r"\\?", "?") - # Special case about: URLs as they are not relative to base_url - if ( - match.startswith("about:") - or match.startswith("data:") - or match.startswith("chrome:") - or match.startswith("edge:") - or match.startswith("file:") - ): - # about: and data: URLs are not relative to base_url, so we return them as is. - return match - # Glob symbols may be escaped in the URL and some of them such as ? affect resolution, - # so we replace them with safe components first. - processed_parts = [] - for index, token in enumerate(match.split("/")): - if token in (".", "..", ""): - processed_parts.append(token) - continue - # Handle special case of http*://, note that the new schema has to be - # a web schema so that slashes are properly inserted after domain. - if index == 0 and token.endswith(":"): - # Replace any pattern with http: - if "*" in token or "{" in token: - processed_parts.append(map_token(token, "http:")) - else: - # Preserve explicit schema as is as it may affect trailing slashes after domain. - processed_parts.append(token) - continue - question_index = token.find("?") - if question_index == -1: - processed_parts.append(map_token(token, f"$_{index}_$")) - else: - new_prefix = map_token(token[:question_index], f"$_{index}_$") - new_suffix = map_token(token[question_index:], f"?$_{index}_$") - processed_parts.append(new_prefix + new_suffix) - - relative_path = "/".join(processed_parts) - resolved, case_insensitive_part = resolve_base_url(base_url, relative_path) - - for token, original in token_map.items(): - normalize = case_insensitive_part and token in case_insensitive_part - resolved = resolved.replace( - token, original.lower() if normalize else original, 1 - ) - - return resolved - - -def resolve_base_url( - base_url: Optional[str], given_url: str -) -> Tuple[str, Optional[str]]: - try: - url = nodelike_urlparse( - urljoin(base_url if base_url is not None else "", given_url) - ) - resolved = urlunparse(url) - # Schema and domain are case-insensitive. - hostname_port = ( - url.hostname or "" - ) # can't use parsed.netloc because it includes userinfo (username:password) - if url.port: - hostname_port += f":{url.port}" - case_insensitive_prefix = f"{url.scheme}://{hostname_port}" - return resolved, case_insensitive_prefix - except Exception: - return given_url, None - - -def nodelike_urlparse(url: str) -> ParseResult: - parsed = urlparse(url, allow_fragments=True) - - # https://url.spec.whatwg.org/#special-scheme - is_special_url = parsed.scheme in ["http", "https", "ws", "wss", "ftp", "file"] - if is_special_url: - # special urls have a list path, list paths are serialized as follows: https://url.spec.whatwg.org/#url-path-serializer - # urllib diverges, so we patch it here - if parsed.path == "": - parsed = parsed._replace(path="/") - - return parsed - - class HarLookupResult(TypedDict, total=False): action: Literal["error", "redirect", "fulfill", "noentry"] message: Optional[str] diff --git a/playwright/_impl/_input.py b/playwright/_impl/_input.py index 8a39242ee..1568885f2 100644 --- a/playwright/_impl/_input.py +++ b/playwright/_impl/_input.py @@ -70,9 +70,9 @@ async def _click( clickCount: int = None, title: str = None, ) -> None: - await self._channel.send( - "mouseClick", None, locals_to_params(locals()), title=title - ) + params = locals_to_params(locals()) + params.pop("title", None) + await self._channel.send("mouseClick", None, params, title=title) async def click( self, diff --git a/playwright/_impl/_network.py b/playwright/_impl/_network.py index 018041947..589bc45dd 100644 --- a/playwright/_impl/_network.py +++ b/playwright/_impl/_network.py @@ -444,6 +444,9 @@ async def _inner_fulfill( response: "APIResponse" = None, ) -> None: params = locals_to_params(locals()) + # These options become the body and the headers below. + for name in ("json", "path", "contentType", "response"): + params.pop(name, None) if json is not None: if body is not None: @@ -451,7 +454,6 @@ async def _inner_fulfill( body = json_utils.dumps(json) if response: - del params["response"] params["status"] = ( params["status"] if params.get("status") else response.status ) @@ -476,15 +478,14 @@ async def _inner_fulfill( params["isBase64"] = True length = len(body) elif path: - del params["path"] file_content = Path(path).read_bytes() params["body"] = base64.b64encode(file_content).decode() params["isBase64"] = True length = len(file_content) headers = {k.lower(): str(v) for k, v in params.get("headers", {}).items()} - if params.get("contentType"): - headers["content-type"] = params["contentType"] + if contentType: + headers["content-type"] = contentType elif json or isinstance(json, (list, tuple, dict)): # Like the JS client, which checks truthiness: [] and {} are truthy in JS. headers["content-type"] = "application/json" diff --git a/playwright/_impl/_protocol.py b/playwright/_impl/_protocol.py new file mode 100644 index 000000000..90aac2129 --- /dev/null +++ b/playwright/_impl/_protocol.py @@ -0,0 +1,2337 @@ +# 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. + +# Generated from packages/protocol/spec by scripts/codegen/generate_protocol.py. Do not edit. + +from typing import Any, Dict + +# The interface that each interface extends. +EXTENDS: Dict[str, str] = {"ElementHandle": "JSHandle"} + +# The object and enum types, in the form of the spec. +TYPES: Dict[str, Any] = { + "AndroidWebView": { + "type": "object", + "properties": {"pid": "int", "pkg": "string", "socketName": "string"}, + }, + "AndroidSelector": { + "type": "object", + "properties": { + "checkable": "boolean?", + "checked": "boolean?", + "clazz": "string?", + "clickable": "boolean?", + "depth": "int?", + "desc": "string?", + "enabled": "boolean?", + "focusable": "boolean?", + "focused": "boolean?", + "hasChild": { + "type": "object?", + "properties": {"androidSelector": "AndroidSelector"}, + }, + "hasDescendant": { + "type": "object?", + "properties": { + "androidSelector": "AndroidSelector", + "maxDepth": "int?", + }, + }, + "longClickable": "boolean?", + "pkg": "string?", + "res": "string?", + "scrollable": "boolean?", + "selected": "boolean?", + "text": "string?", + }, + }, + "AndroidElementInfo": { + "type": "object", + "properties": { + "children": {"type": "array?", "items": "AndroidElementInfo"}, + "clazz": "string", + "desc": "string", + "res": "string", + "pkg": "string", + "text": "string", + "bounds": "Rect", + "checkable": "boolean", + "checked": "boolean", + "clickable": "boolean", + "enabled": "boolean", + "focusable": "boolean", + "focused": "boolean", + "longClickable": "boolean", + "scrollable": "boolean", + "selected": "boolean", + }, + }, + "APIResponse": { + "type": "object", + "properties": { + "fetchUid": "string", + "url": "string", + "status": "int", + "statusText": "string", + "headers": {"type": "array", "items": "NameValue"}, + "securityDetails": "SecurityDetails?", + "serverAddr": "RemoteAddr?", + "timing": "ResourceTiming?", + "responseEndTiming": "float?", + }, + }, + "Metadata": { + "type": "object", + "properties": { + "location": { + "type": "object?", + "properties": {"file": "string", "line": "int?", "column": "int?"}, + }, + "title": "string?", + "internal": "boolean?", + "timeout": "float?", + }, + }, + "ClientSideCallMetadata": { + "type": "object", + "properties": { + "id": "string", + "stack": {"type": "array?", "items": "StackFrame"}, + }, + }, + "SDKLanguage": { + "type": "enum", + "literals": ["javascript", "python", "java", "csharp"], + }, + "WaitInfo": { + "type": "object", + "properties": { + "waitId": "string", + "phase": {"type": "enum", "literals": ["before", "after", "log"]}, + "event": "string?", + "message": "string?", + "error": "string?", + }, + }, + "HttpCredentials": { + "type": "object", + "properties": { + "username": "string", + "password": "string", + "origin": "string?", + "send": {"type": "enum?", "literals": ["always", "unauthorized"]}, + }, + }, + "SetNetworkCookie": { + "type": "object", + "properties": { + "name": "string", + "value": "string", + "url": "string?", + "domain": "string?", + "path": "string?", + "expires": "float?", + "httpOnly": "boolean?", + "secure": "boolean?", + "sameSite": {"type": "enum?", "literals": ["Strict", "Lax", "None"]}, + "partitionKey": "string?", + "_crHasCrossSiteAncestor": "boolean?", + }, + }, + "NetworkCookie": { + "type": "object", + "properties": { + "name": "string", + "value": "string", + "domain": "string", + "path": "string", + "expires": "float", + "httpOnly": "boolean", + "secure": "boolean", + "sameSite": {"type": "enum", "literals": ["Strict", "Lax", "None"]}, + "partitionKey": "string?", + "_crHasCrossSiteAncestor": "boolean?", + }, + }, + "ResourceTiming": { + "type": "object", + "properties": { + "startTime": "float", + "domainLookupStart": "float", + "domainLookupEnd": "float", + "connectStart": "float", + "secureConnectionStart": "float", + "connectEnd": "float", + "requestStart": "float", + "responseStart": "float", + }, + }, + "SecurityDetails": { + "type": "object", + "properties": { + "issuer": "string?", + "protocol": "string?", + "subjectName": "string?", + "validFrom": "float?", + "validTo": "float?", + }, + }, + "RequestSizes": { + "type": "object", + "properties": { + "requestBodySize": "int", + "requestHeadersSize": "int", + "responseBodySize": "int", + "responseHeadersSize": "int", + }, + }, + "RemoteAddr": { + "type": "object", + "properties": {"ipAddress": "string", "port": "int"}, + }, + "ExpectedTextValue": { + "type": "object", + "properties": { + "string": "string?", + "regexSource": "string?", + "regexFlags": "string?", + "matchSubstring": "boolean?", + "ignoreCase": "boolean?", + "normalizeWhiteSpace": "boolean?", + }, + }, + "SelectorEngine": { + "type": "object", + "properties": { + "name": "string", + "source": "string", + "contentScript": "boolean?", + }, + }, + "FormField": { + "type": "object", + "properties": { + "name": "string", + "value": "string?", + "file": { + "type": "object?", + "properties": { + "name": "string", + "mimeType": "string?", + "buffer": "binary", + }, + }, + }, + }, + "LifecycleEvent": { + "type": "enum", + "literals": ["load", "domcontentloaded", "networkidle", "commit"], + }, + "ConsoleMessagesFilter": {"type": "enum", "literals": ["all", "since-navigation"]}, + "RecorderSource": { + "type": "object", + "properties": { + "isRecorded": "boolean", + "id": "string", + "label": "string", + "text": "string", + "language": "string", + "highlight": { + "type": "array", + "items": { + "type": "object", + "properties": {"line": "int", "type": "string"}, + }, + }, + "revealLine": "int?", + "group": "string?", + }, + }, + "IndexedDBDatabase": { + "type": "object", + "properties": { + "name": "string", + "version": "int", + "stores": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": "string", + "autoIncrement": "boolean", + "keyPath": "string?", + "keyPathArray": {"type": "array?", "items": "string"}, + "records": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": "json?", + "keyEncoded": "json?", + "value": "json?", + "valueEncoded": "json?", + }, + }, + }, + "indexes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": "string", + "keyPath": "string?", + "keyPathArray": { + "type": "array?", + "items": "string", + }, + "multiEntry": "boolean", + "unique": "boolean", + }, + }, + }, + }, + }, + }, + }, + }, + "OPFSEntry": { + "type": "object", + "properties": { + "path": "string", + "type": {"type": "enum", "literals": ["file", "directory"]}, + "base64": "string?", + }, + }, + "SetOriginStorage": { + "type": "object", + "properties": { + "origin": "string", + "localStorage": {"type": "array", "items": "NameValue"}, + "indexedDB": {"type": "array?", "items": "IndexedDBDatabase"}, + "opfs": {"type": "array?", "items": "OPFSEntry"}, + }, + }, + "OriginStorage": { + "type": "object", + "properties": { + "origin": "string", + "localStorage": {"type": "array", "items": "NameValue"}, + "indexedDB": {"type": "array?", "items": "IndexedDBDatabase"}, + "opfs": {"type": "array?", "items": "OPFSEntry"}, + }, + }, + "RecordHarOptions": { + "type": "object", + "properties": { + "content": {"type": "enum?", "literals": ["embed", "attach", "omit"]}, + "mode": {"type": "enum?", "literals": ["full", "minimal"]}, + "urlGlob": "string?", + "urlRegexSource": "string?", + "urlRegexFlags": "string?", + "harPath": "string?", + "resourcesDir": "string?", + }, + }, + "SerializedValue": { + "type": "object", + "properties": { + "n": "float?", + "b": "boolean?", + "s": "string?", + "v": { + "type": "enum?", + "literals": ["null", "undefined", "NaN", "Infinity", "-Infinity", "-0"], + }, + "d": "string?", + "u": "string?", + "bi": "string?", + "ta": { + "type": "object?", + "properties": { + "b": "binary", + "k": { + "type": "enum", + "literals": [ + "i8", + "ui8", + "ui8c", + "i16", + "ui16", + "i32", + "ui32", + "f32", + "f64", + "bi64", + "bui64", + ], + }, + }, + }, + "e": { + "type": "object?", + "properties": {"m": "string", "n": "string", "s": "string"}, + }, + "r": {"type": "object?", "properties": {"p": "string", "f": "string"}}, + "a": {"type": "array?", "items": "SerializedValue"}, + "o": { + "type": "array?", + "items": { + "type": "object", + "properties": {"k": "string", "v": "SerializedValue"}, + }, + }, + "h": "int?", + "fn": "string?", + "id": "int?", + "ref": "int?", + }, + }, + "SerializedArgument": { + "type": "object", + "properties": { + "value": "SerializedValue", + "handles": {"type": "array", "items": "Channel"}, + }, + }, + "SerializedError": { + "type": "object", + "properties": { + "error": { + "type": "object?", + "properties": { + "message": "string", + "name": "string", + "stack": "string?", + "code": "string?", + }, + }, + "value": "SerializedValue?", + }, + }, + "StackFrame": { + "type": "object", + "properties": { + "file": "string", + "line": "int", + "column": "int", + "function": "string?", + }, + }, + "VirtualCredential": { + "type": "object", + "properties": { + "id": "string", + "rpId": "string", + "userHandle": "string", + "privateKey": "string", + "publicKey": "string", + "signCount": "int", + }, + }, + "SetVirtualCredential": { + "type": "object", + "properties": { + "id": "string", + "rpId": "string", + "userHandle": "string", + "privateKey": "string", + "publicKey": "string", + "signCount": "int?", + }, + }, + "Point": {"type": "object", "properties": {"x": "float", "y": "float"}}, + "Rect": { + "type": "object", + "properties": {"x": "float", "y": "float", "width": "float", "height": "float"}, + }, + "URLPattern": { + "type": "object", + "properties": { + "hash": "string", + "hostname": "string", + "password": "string", + "pathname": "string", + "port": "string", + "protocol": "string", + "search": "string", + "username": "string", + }, + }, + "NameValue": { + "type": "object", + "properties": {"name": "string", "value": "string"}, + }, +} + +# The parameters of each command, by interface and method. +COMMANDS: Dict[str, Dict[str, Dict[str, Any]]] = { + "Android": { + "devices": {"host": "string?", "port": "int?", "omitDriverInstall": "boolean?"} + }, + "AndroidSocket": {"write": {"data": "binary"}, "close": {}}, + "AndroidDevice": { + "wait": { + "androidSelector": "AndroidSelector", + "state": {"type": "enum?", "literals": ["gone"]}, + }, + "fill": {"androidSelector": "AndroidSelector", "text": "string"}, + "tap": {"androidSelector": "AndroidSelector", "duration": "float?"}, + "drag": { + "androidSelector": "AndroidSelector", + "dest": "Point", + "speed": "float?", + }, + "fling": { + "androidSelector": "AndroidSelector", + "direction": {"type": "enum", "literals": ["up", "down", "left", "right"]}, + "speed": "float?", + }, + "longTap": {"androidSelector": "AndroidSelector"}, + "pinchClose": { + "androidSelector": "AndroidSelector", + "percent": "float", + "speed": "float?", + }, + "pinchOpen": { + "androidSelector": "AndroidSelector", + "percent": "float", + "speed": "float?", + }, + "scroll": { + "androidSelector": "AndroidSelector", + "direction": {"type": "enum", "literals": ["up", "down", "left", "right"]}, + "percent": "float", + "speed": "float?", + }, + "swipe": { + "androidSelector": "AndroidSelector", + "direction": {"type": "enum", "literals": ["up", "down", "left", "right"]}, + "percent": "float", + "speed": "float?", + }, + "info": {"androidSelector": "AndroidSelector"}, + "screenshot": {}, + "inputType": {"text": "string"}, + "inputPress": {"key": "string"}, + "inputTap": {"point": "Point"}, + "inputSwipe": {"segments": {"type": "array", "items": "Point"}, "steps": "int"}, + "inputDrag": {"from": "Point", "to": "Point", "steps": "int"}, + "launchBrowser": { + "noDefaultViewport": "boolean?", + "viewport": { + "type": "object?", + "properties": {"width": "int", "height": "int"}, + }, + "screen": { + "type": "object?", + "properties": {"width": "int", "height": "int"}, + }, + "ignoreHTTPSErrors": "boolean?", + "clientCertificates": { + "type": "array?", + "items": { + "type": "object", + "properties": { + "origin": "string", + "cert": "binary?", + "key": "binary?", + "passphrase": "string?", + "pfx": "binary?", + }, + }, + }, + "javaScriptEnabled": "boolean?", + "bypassCSP": "boolean?", + "userAgent": "string?", + "locale": "string?", + "timezoneId": "string?", + "geolocation": { + "type": "object?", + "properties": { + "longitude": "float", + "latitude": "float", + "accuracy": "float?", + }, + }, + "permissions": {"type": "array?", "items": "string"}, + "extraHTTPHeaders": {"type": "array?", "items": "NameValue"}, + "offline": "boolean?", + "httpCredentials": {"type": "array?", "items": "HttpCredentials"}, + "deviceScaleFactor": "float?", + "isMobile": "boolean?", + "hasTouch": "boolean?", + "colorScheme": { + "type": "enum?", + "literals": ["dark", "light", "no-preference", "no-override"], + }, + "reducedMotion": { + "type": "enum?", + "literals": ["reduce", "no-preference", "no-override"], + }, + "forcedColors": { + "type": "enum?", + "literals": ["active", "none", "no-override"], + }, + "acceptDownloads": { + "type": "enum?", + "literals": ["accept", "deny", "internal-browser-default"], + }, + "contrast": { + "type": "enum?", + "literals": ["no-preference", "more", "no-override"], + }, + "baseURL": "string?", + "recordVideo": { + "type": "object?", + "properties": { + "dir": "string?", + "size": { + "type": "object?", + "properties": {"width": "int", "height": "int"}, + }, + "fps": "int?", + "showActions": { + "type": "object?", + "properties": { + "duration": "float?", + "position": { + "type": "enum?", + "literals": [ + "top-left", + "top", + "top-right", + "bottom-left", + "bottom", + "bottom-right", + ], + }, + "fontSize": "int?", + "cursor": { + "type": "enum?", + "literals": ["none", "pointer"], + }, + "style": { + "type": "object?", + "properties": { + "point": "string?", + "highlight": "string?", + "title": "string?", + }, + }, + }, + }, + }, + }, + "strictSelectors": "boolean?", + "serviceWorkers": {"type": "enum?", "literals": ["allow", "block"]}, + "selectorEngines": {"type": "array?", "items": "SelectorEngine"}, + "testIdAttributeName": "string?", + "pkg": "string?", + "args": {"type": "array?", "items": "string"}, + "proxy": { + "type": "object?", + "properties": { + "server": "string", + "bypass": "string?", + "username": "string?", + "password": "string?", + }, + }, + }, + "open": {"command": "string"}, + "shell": {"command": "string"}, + "installApk": {"file": "binary", "args": {"type": "array?", "items": "string"}}, + "push": {"file": "binary", "path": "string", "mode": "int?"}, + "connectToWebView": {"socketName": "string"}, + "close": {}, + }, + "APIRequestContext": { + "fetch": { + "url": "string", + "encodedParams": "string?", + "params": {"type": "array?", "items": "NameValue"}, + "method": "string?", + "headers": {"type": "array?", "items": "NameValue"}, + "postData": "binary?", + "jsonData": "string?", + "formData": {"type": "array?", "items": "NameValue"}, + "multipartData": {"type": "array?", "items": "FormField"}, + "failOnStatusCode": "boolean?", + "ignoreHTTPSErrors": "boolean?", + "maxRedirects": "int?", + "maxRetries": "int?", + }, + "fetchResponseBody": {"fetchUid": "string"}, + "fetchLog": {"fetchUid": "string"}, + "addCookies": {"cookies": {"type": "array", "items": "SetNetworkCookie"}}, + "clearCookies": { + "name": "string?", + "nameRegexSource": "string?", + "nameRegexFlags": "string?", + "domain": "string?", + "domainRegexSource": "string?", + "domainRegexFlags": "string?", + "path": "string?", + "pathRegexSource": "string?", + "pathRegexFlags": "string?", + }, + "cookies": {"urls": {"type": "array", "items": "string"}}, + "storageState": {"indexedDB": "boolean?", "opfs": "boolean?"}, + "disposeAPIResponse": {"fetchUid": "string"}, + "dispose": {"reason": "string?"}, + }, + "Artifact": { + "pathAfterFinished": {}, + "saveAs": {"path": "string"}, + "saveAsStream": {}, + "failure": {}, + "stream": {}, + "cancel": {}, + "delete": {}, + }, + "Stream": {"read": {"size": "int?"}, "close": {}}, + "WritableStream": {"write": {"binary": "binary"}, "close": {}}, + "Browser": { + "startServer": { + "title": "string", + "workspaceDir": "string?", + "metadata": "json?", + "host": "string?", + "port": "int?", + }, + "stopServer": {}, + "close": {"reason": "string?"}, + "killForTests": {}, + "defaultUserAgentForTest": {}, + "newContext": { + "noDefaultViewport": "boolean?", + "viewport": { + "type": "object?", + "properties": {"width": "int", "height": "int"}, + }, + "screen": { + "type": "object?", + "properties": {"width": "int", "height": "int"}, + }, + "ignoreHTTPSErrors": "boolean?", + "clientCertificates": { + "type": "array?", + "items": { + "type": "object", + "properties": { + "origin": "string", + "cert": "binary?", + "key": "binary?", + "passphrase": "string?", + "pfx": "binary?", + }, + }, + }, + "javaScriptEnabled": "boolean?", + "bypassCSP": "boolean?", + "userAgent": "string?", + "locale": "string?", + "timezoneId": "string?", + "geolocation": { + "type": "object?", + "properties": { + "longitude": "float", + "latitude": "float", + "accuracy": "float?", + }, + }, + "permissions": {"type": "array?", "items": "string"}, + "extraHTTPHeaders": {"type": "array?", "items": "NameValue"}, + "offline": "boolean?", + "httpCredentials": {"type": "array?", "items": "HttpCredentials"}, + "deviceScaleFactor": "float?", + "isMobile": "boolean?", + "hasTouch": "boolean?", + "colorScheme": { + "type": "enum?", + "literals": ["dark", "light", "no-preference", "no-override"], + }, + "reducedMotion": { + "type": "enum?", + "literals": ["reduce", "no-preference", "no-override"], + }, + "forcedColors": { + "type": "enum?", + "literals": ["active", "none", "no-override"], + }, + "acceptDownloads": { + "type": "enum?", + "literals": ["accept", "deny", "internal-browser-default"], + }, + "contrast": { + "type": "enum?", + "literals": ["no-preference", "more", "no-override"], + }, + "baseURL": "string?", + "recordVideo": { + "type": "object?", + "properties": { + "dir": "string?", + "size": { + "type": "object?", + "properties": {"width": "int", "height": "int"}, + }, + "fps": "int?", + "showActions": { + "type": "object?", + "properties": { + "duration": "float?", + "position": { + "type": "enum?", + "literals": [ + "top-left", + "top", + "top-right", + "bottom-left", + "bottom", + "bottom-right", + ], + }, + "fontSize": "int?", + "cursor": { + "type": "enum?", + "literals": ["none", "pointer"], + }, + "style": { + "type": "object?", + "properties": { + "point": "string?", + "highlight": "string?", + "title": "string?", + }, + }, + }, + }, + }, + }, + "strictSelectors": "boolean?", + "serviceWorkers": {"type": "enum?", "literals": ["allow", "block"]}, + "selectorEngines": {"type": "array?", "items": "SelectorEngine"}, + "testIdAttributeName": "string?", + "proxy": { + "type": "object?", + "properties": { + "server": "string", + "bypass": "string?", + "username": "string?", + "password": "string?", + }, + }, + "storageState": { + "type": "object?", + "properties": { + "cookies": {"type": "array?", "items": "SetNetworkCookie"}, + "origins": {"type": "array?", "items": "SetOriginStorage"}, + "credentials": {"type": "array?", "items": "SetVirtualCredential"}, + }, + }, + }, + "newContextForReuse": { + "noDefaultViewport": "boolean?", + "viewport": { + "type": "object?", + "properties": {"width": "int", "height": "int"}, + }, + "screen": { + "type": "object?", + "properties": {"width": "int", "height": "int"}, + }, + "ignoreHTTPSErrors": "boolean?", + "clientCertificates": { + "type": "array?", + "items": { + "type": "object", + "properties": { + "origin": "string", + "cert": "binary?", + "key": "binary?", + "passphrase": "string?", + "pfx": "binary?", + }, + }, + }, + "javaScriptEnabled": "boolean?", + "bypassCSP": "boolean?", + "userAgent": "string?", + "locale": "string?", + "timezoneId": "string?", + "geolocation": { + "type": "object?", + "properties": { + "longitude": "float", + "latitude": "float", + "accuracy": "float?", + }, + }, + "permissions": {"type": "array?", "items": "string"}, + "extraHTTPHeaders": {"type": "array?", "items": "NameValue"}, + "offline": "boolean?", + "httpCredentials": {"type": "array?", "items": "HttpCredentials"}, + "deviceScaleFactor": "float?", + "isMobile": "boolean?", + "hasTouch": "boolean?", + "colorScheme": { + "type": "enum?", + "literals": ["dark", "light", "no-preference", "no-override"], + }, + "reducedMotion": { + "type": "enum?", + "literals": ["reduce", "no-preference", "no-override"], + }, + "forcedColors": { + "type": "enum?", + "literals": ["active", "none", "no-override"], + }, + "acceptDownloads": { + "type": "enum?", + "literals": ["accept", "deny", "internal-browser-default"], + }, + "contrast": { + "type": "enum?", + "literals": ["no-preference", "more", "no-override"], + }, + "baseURL": "string?", + "recordVideo": { + "type": "object?", + "properties": { + "dir": "string?", + "size": { + "type": "object?", + "properties": {"width": "int", "height": "int"}, + }, + "fps": "int?", + "showActions": { + "type": "object?", + "properties": { + "duration": "float?", + "position": { + "type": "enum?", + "literals": [ + "top-left", + "top", + "top-right", + "bottom-left", + "bottom", + "bottom-right", + ], + }, + "fontSize": "int?", + "cursor": { + "type": "enum?", + "literals": ["none", "pointer"], + }, + "style": { + "type": "object?", + "properties": { + "point": "string?", + "highlight": "string?", + "title": "string?", + }, + }, + }, + }, + }, + }, + "strictSelectors": "boolean?", + "serviceWorkers": {"type": "enum?", "literals": ["allow", "block"]}, + "selectorEngines": {"type": "array?", "items": "SelectorEngine"}, + "testIdAttributeName": "string?", + "proxy": { + "type": "object?", + "properties": { + "server": "string", + "bypass": "string?", + "username": "string?", + "password": "string?", + }, + }, + "storageState": { + "type": "object?", + "properties": { + "cookies": {"type": "array?", "items": "SetNetworkCookie"}, + "origins": {"type": "array?", "items": "SetOriginStorage"}, + "credentials": {"type": "array?", "items": "SetVirtualCredential"}, + }, + }, + }, + "disconnectFromReusedContext": {"reason": "string"}, + "newBrowserCDPSession": {}, + "startTracing": { + "page": "Page?", + "screenshots": "boolean?", + "categories": {"type": "array?", "items": "string"}, + }, + "stopTracing": {}, + }, + "BrowserContext": { + "addCookies": {"cookies": {"type": "array", "items": "SetNetworkCookie"}}, + "addInitScript": {"source": "string"}, + "clearCookies": { + "name": "string?", + "nameRegexSource": "string?", + "nameRegexFlags": "string?", + "domain": "string?", + "domainRegexSource": "string?", + "domainRegexFlags": "string?", + "path": "string?", + "pathRegexSource": "string?", + "pathRegexFlags": "string?", + }, + "clearPermissions": {}, + "close": {"reason": "string?"}, + "cookies": {"urls": {"type": "array", "items": "string"}}, + "exposeBinding": {"name": "string", "noGlobal": "boolean?"}, + "grantPermissions": { + "permissions": {"type": "array", "items": "string"}, + "origin": "string?", + }, + "newPage": {}, + "registerSelectorEngine": {"selectorEngine": "SelectorEngine"}, + "setTestIdAttributeName": {"testIdAttributeName": "string"}, + "setExtraHTTPHeaders": {"headers": {"type": "array", "items": "NameValue"}}, + "setGeolocation": { + "geolocation": { + "type": "object?", + "properties": { + "longitude": "float", + "latitude": "float", + "accuracy": "float?", + }, + } + }, + "setHTTPCredentials": { + "httpCredentials": {"type": "array?", "items": "HttpCredentials"} + }, + "setNetworkInterceptionPatterns": { + "patterns": { + "type": "array", + "items": { + "type": "object", + "properties": { + "glob": "string?", + "regexSource": "string?", + "regexFlags": "string?", + "urlPattern": "URLPattern?", + }, + }, + } + }, + "setWebSocketInterceptionPatterns": { + "patterns": { + "type": "array", + "items": { + "type": "object", + "properties": { + "glob": "string?", + "regexSource": "string?", + "regexFlags": "string?", + "urlPattern": "URLPattern?", + }, + }, + } + }, + "setOffline": {"offline": "boolean"}, + "storageState": { + "indexedDB": "boolean?", + "opfs": "boolean?", + "credentials": "boolean?", + }, + "setStorageState": { + "storageState": { + "type": "object?", + "properties": { + "cookies": {"type": "array?", "items": "SetNetworkCookie"}, + "origins": {"type": "array?", "items": "SetOriginStorage"}, + "credentials": {"type": "array?", "items": "SetVirtualCredential"}, + }, + } + }, + "pause": {}, + "showRecorder": { + "language": "string?", + "mode": {"type": "enum?", "literals": ["inspecting", "recording"]}, + "pauseOnNextStatement": "boolean?", + "testIdAttributeName": "string?", + "launchOptions": "json?", + "contextOptions": "json?", + "device": "string?", + "saveStorage": "string?", + "outputFile": "string?", + "handleSIGINT": "boolean?", + "omitCallTracking": "boolean?", + }, + "startRecording": {"language": "string?"}, + "stopRecording": {}, + "exposeConsoleApi": {}, + "newCDPSession": {"page": "Page?", "frame": "Frame?"}, + "createTempFiles": { + "rootDirName": "string?", + "items": { + "type": "array", + "items": { + "type": "object", + "properties": {"name": "string", "lastModifiedMs": "float?"}, + }, + }, + }, + "updateSubscription": { + "event": { + "type": "enum", + "literals": [ + "console", + "dialog", + "dialogClosed", + "request", + "response", + "requestFinished", + "requestFailed", + ], + }, + "enabled": "boolean", + }, + "clockFastForward": {"ticksNumber": "float?", "ticksString": "string?"}, + "clockInstall": {"timeNumber": "float?", "timeString": "string?"}, + "clockPauseAt": {"timeNumber": "float?", "timeString": "string?"}, + "clockResume": {}, + "clockRunFor": {"ticksNumber": "float?", "ticksString": "string?"}, + "clockSetFixedTime": {"timeNumber": "float?", "timeString": "string?"}, + "clockSetSystemTime": {"timeNumber": "float?", "timeString": "string?"}, + "credentialsInstall": {}, + "credentialsCreate": { + "rpId": "string", + "id": "string?", + "userHandle": "string?", + "privateKey": "string?", + "publicKey": "string?", + "signCount": "int?", + }, + "credentialsGet": {"rpId": "string?", "id": "string?"}, + "credentialsDelete": {"id": "string"}, + }, + "BrowserType": { + "launch": { + "channel": "string?", + "executablePath": "string?", + "args": {"type": "array?", "items": "string"}, + "ignoreAllDefaultArgs": "boolean?", + "ignoreDefaultArgs": {"type": "array?", "items": "string"}, + "handleSIGINT": "boolean?", + "handleSIGTERM": "boolean?", + "handleSIGHUP": "boolean?", + "env": {"type": "array?", "items": "NameValue"}, + "headless": "boolean?", + "proxy": { + "type": "object?", + "properties": { + "server": "string", + "bypass": "string?", + "username": "string?", + "password": "string?", + }, + }, + "downloadsPath": "string?", + "tracesDir": "string?", + "artifactsDir": "string?", + "chromiumSandbox": "boolean?", + "firefoxUserPrefs": "json?", + "cdpPort": "int?", + "slowMo": "float?", + }, + "launchPersistentContext": { + "channel": "string?", + "executablePath": "string?", + "args": {"type": "array?", "items": "string"}, + "ignoreAllDefaultArgs": "boolean?", + "ignoreDefaultArgs": {"type": "array?", "items": "string"}, + "handleSIGINT": "boolean?", + "handleSIGTERM": "boolean?", + "handleSIGHUP": "boolean?", + "env": {"type": "array?", "items": "NameValue"}, + "headless": "boolean?", + "proxy": { + "type": "object?", + "properties": { + "server": "string", + "bypass": "string?", + "username": "string?", + "password": "string?", + }, + }, + "downloadsPath": "string?", + "tracesDir": "string?", + "artifactsDir": "string?", + "chromiumSandbox": "boolean?", + "firefoxUserPrefs": "json?", + "cdpPort": "int?", + "noDefaultViewport": "boolean?", + "viewport": { + "type": "object?", + "properties": {"width": "int", "height": "int"}, + }, + "screen": { + "type": "object?", + "properties": {"width": "int", "height": "int"}, + }, + "ignoreHTTPSErrors": "boolean?", + "clientCertificates": { + "type": "array?", + "items": { + "type": "object", + "properties": { + "origin": "string", + "cert": "binary?", + "key": "binary?", + "passphrase": "string?", + "pfx": "binary?", + }, + }, + }, + "javaScriptEnabled": "boolean?", + "bypassCSP": "boolean?", + "userAgent": "string?", + "locale": "string?", + "timezoneId": "string?", + "geolocation": { + "type": "object?", + "properties": { + "longitude": "float", + "latitude": "float", + "accuracy": "float?", + }, + }, + "permissions": {"type": "array?", "items": "string"}, + "extraHTTPHeaders": {"type": "array?", "items": "NameValue"}, + "offline": "boolean?", + "httpCredentials": {"type": "array?", "items": "HttpCredentials"}, + "deviceScaleFactor": "float?", + "isMobile": "boolean?", + "hasTouch": "boolean?", + "colorScheme": { + "type": "enum?", + "literals": ["dark", "light", "no-preference", "no-override"], + }, + "reducedMotion": { + "type": "enum?", + "literals": ["reduce", "no-preference", "no-override"], + }, + "forcedColors": { + "type": "enum?", + "literals": ["active", "none", "no-override"], + }, + "acceptDownloads": { + "type": "enum?", + "literals": ["accept", "deny", "internal-browser-default"], + }, + "contrast": { + "type": "enum?", + "literals": ["no-preference", "more", "no-override"], + }, + "baseURL": "string?", + "recordVideo": { + "type": "object?", + "properties": { + "dir": "string?", + "size": { + "type": "object?", + "properties": {"width": "int", "height": "int"}, + }, + "fps": "int?", + "showActions": { + "type": "object?", + "properties": { + "duration": "float?", + "position": { + "type": "enum?", + "literals": [ + "top-left", + "top", + "top-right", + "bottom-left", + "bottom", + "bottom-right", + ], + }, + "fontSize": "int?", + "cursor": { + "type": "enum?", + "literals": ["none", "pointer"], + }, + "style": { + "type": "object?", + "properties": { + "point": "string?", + "highlight": "string?", + "title": "string?", + }, + }, + }, + }, + }, + }, + "strictSelectors": "boolean?", + "serviceWorkers": {"type": "enum?", "literals": ["allow", "block"]}, + "selectorEngines": {"type": "array?", "items": "SelectorEngine"}, + "testIdAttributeName": "string?", + "userDataDir": "string", + "slowMo": "float?", + }, + "connectOverCDP": { + "endpointURL": "string?", + "headers": {"type": "array?", "items": "NameValue"}, + "slowMo": "float?", + "isLocal": "boolean?", + "noDefaults": "boolean?", + "isWebView": "boolean?", + "artifactsDir": "string?", + "transport": "binary?", + }, + "connectToWorker": {"endpoint": "string"}, + }, + "Disposable": {"dispose": {}}, + "Electron": { + "launch": { + "executablePath": "string?", + "args": {"type": "array?", "items": "string"}, + "chromiumSandbox": "boolean?", + "cwd": "string?", + "env": {"type": "array?", "items": "NameValue"}, + "acceptDownloads": { + "type": "enum?", + "literals": ["accept", "deny", "internal-browser-default"], + }, + "bypassCSP": "boolean?", + "colorScheme": { + "type": "enum?", + "literals": ["dark", "light", "no-preference", "no-override"], + }, + "extraHTTPHeaders": {"type": "array?", "items": "NameValue"}, + "geolocation": { + "type": "object?", + "properties": { + "longitude": "float", + "latitude": "float", + "accuracy": "float?", + }, + }, + "httpCredentials": {"type": "array?", "items": "HttpCredentials"}, + "ignoreHTTPSErrors": "boolean?", + "locale": "string?", + "offline": "boolean?", + "recordVideo": { + "type": "object?", + "properties": { + "dir": "string?", + "size": { + "type": "object?", + "properties": {"width": "int", "height": "int"}, + }, + "fps": "int?", + "showActions": { + "type": "object?", + "properties": { + "duration": "float?", + "position": { + "type": "enum?", + "literals": [ + "top-left", + "top", + "top-right", + "bottom-left", + "bottom", + "bottom-right", + ], + }, + "fontSize": "int?", + "cursor": { + "type": "enum?", + "literals": ["none", "pointer"], + }, + "style": { + "type": "object?", + "properties": { + "point": "string?", + "highlight": "string?", + "title": "string?", + }, + }, + }, + }, + }, + }, + "strictSelectors": "boolean?", + "timezoneId": "string?", + "tracesDir": "string?", + "artifactsDir": "string?", + "selectorEngines": {"type": "array?", "items": "SelectorEngine"}, + "testIdAttributeName": "string?", + } + }, + "ElectronApplication": { + "browserWindow": {"page": "Page"}, + "evaluateExpression": { + "expression": "string", + "isFunction": "boolean?", + "arg": "SerializedArgument", + }, + "evaluateExpressionHandle": { + "expression": "string", + "isFunction": "boolean?", + "arg": "SerializedArgument", + }, + "updateSubscription": { + "event": {"type": "enum", "literals": ["console"]}, + "enabled": "boolean", + }, + }, + "Frame": { + "evalOnSelector": { + "selector": "string", + "strict": "boolean?", + "expression": "string", + "isFunction": "boolean?", + "arg": "SerializedArgument", + }, + "evalOnSelectorAll": { + "selector": "string", + "expression": "string", + "isFunction": "boolean?", + "arg": "SerializedArgument", + }, + "addScriptTag": {"url": "string?", "content": "string?", "type": "string?"}, + "addStyleTag": {"url": "string?", "content": "string?"}, + "ariaSnapshot": { + "mode": {"type": "enum?", "literals": ["ai", "default"]}, + "selector": "string?", + "depth": "int?", + "boxes": "boolean?", + }, + "ariaSnapshotJSON": { + "mode": {"type": "enum?", "literals": ["ai", "default"]}, + "selector": "string?", + "depth": "int?", + "boxes": "boolean?", + }, + "blur": {"selector": "string", "strict": "boolean?"}, + "check": { + "selector": "string", + "strict": "boolean?", + "force": "boolean?", + "scroll": {"type": "enum?", "literals": ["auto", "none"]}, + "position": "Point?", + "trial": "boolean?", + }, + "click": { + "selector": "string", + "strict": "boolean?", + "force": "boolean?", + "scroll": {"type": "enum?", "literals": ["auto", "none"]}, + "noWaitAfter": "boolean?", + "modifiers": { + "type": "array?", + "items": { + "type": "enum", + "literals": ["Alt", "Control", "ControlOrMeta", "Meta", "Shift"], + }, + }, + "position": "Point?", + "delay": "float?", + "button": {"type": "enum?", "literals": ["left", "right", "middle"]}, + "clickCount": "int?", + "trial": "boolean?", + "steps": "int?", + }, + "content": {"includeShadow": "boolean?"}, + "dragAndDrop": { + "source": "string", + "target": "string", + "force": "boolean?", + "scroll": {"type": "enum?", "literals": ["auto", "none"]}, + "trial": "boolean?", + "sourcePosition": "Point?", + "targetPosition": "Point?", + "strict": "boolean?", + "steps": "int?", + }, + "drop": { + "selector": "string", + "strict": "boolean?", + "position": "Point?", + "payloads": { + "type": "array?", + "items": { + "type": "object", + "properties": { + "name": "string", + "mimeType": "string?", + "buffer": "binary", + }, + }, + }, + "localPaths": {"type": "array?", "items": "string"}, + "streams": {"type": "array?", "items": "WritableStream"}, + "data": { + "type": "array?", + "items": { + "type": "object", + "properties": {"mimeType": "string", "value": "string"}, + }, + }, + }, + "dblclick": { + "selector": "string", + "strict": "boolean?", + "force": "boolean?", + "scroll": {"type": "enum?", "literals": ["auto", "none"]}, + "modifiers": { + "type": "array?", + "items": { + "type": "enum", + "literals": ["Alt", "Control", "ControlOrMeta", "Meta", "Shift"], + }, + }, + "position": "Point?", + "delay": "float?", + "button": {"type": "enum?", "literals": ["left", "right", "middle"]}, + "trial": "boolean?", + "steps": "int?", + }, + "dispatchEvent": { + "selector": "string", + "strict": "boolean?", + "type": "string", + "eventInit": "SerializedArgument", + }, + "evaluateExpression": { + "expression": "string", + "isFunction": "boolean?", + "arg": "SerializedArgument", + }, + "evaluateExpressionHandle": { + "expression": "string", + "isFunction": "boolean?", + "arg": "SerializedArgument", + }, + "fill": { + "selector": "string", + "strict": "boolean?", + "value": "string", + "force": "boolean?", + }, + "focus": {"selector": "string", "strict": "boolean?"}, + "frameElement": {}, + "resolveSelector": {"selector": "string", "sdkLanguage": "SDKLanguage?"}, + "highlight": {"selector": "string", "style": "string?"}, + "hideHighlight": {"selector": "string"}, + "getAttribute": {"selector": "string", "strict": "boolean?", "name": "string"}, + "goto": {"url": "string", "waitUntil": "LifecycleEvent?", "referer": "string?"}, + "hover": { + "selector": "string", + "strict": "boolean?", + "force": "boolean?", + "scroll": {"type": "enum?", "literals": ["auto", "none"]}, + "modifiers": { + "type": "array?", + "items": { + "type": "enum", + "literals": ["Alt", "Control", "ControlOrMeta", "Meta", "Shift"], + }, + }, + "position": "Point?", + "trial": "boolean?", + }, + "innerHTML": {"selector": "string", "strict": "boolean?"}, + "innerText": {"selector": "string", "strict": "boolean?"}, + "inputValue": {"selector": "string", "strict": "boolean?"}, + "isChecked": {"selector": "string", "strict": "boolean?"}, + "isDisabled": {"selector": "string", "strict": "boolean?"}, + "isEnabled": {"selector": "string", "strict": "boolean?"}, + "isHidden": {"selector": "string", "strict": "boolean?"}, + "isVisible": {"selector": "string", "strict": "boolean?"}, + "isEditable": {"selector": "string", "strict": "boolean?"}, + "press": { + "selector": "string", + "strict": "boolean?", + "key": "string", + "delay": "float?", + "noWaitAfter": "boolean?", + }, + "querySelector": {"selector": "string", "strict": "boolean?"}, + "querySelectorAll": {"selector": "string"}, + "queryCount": {"selector": "string"}, + "selectOption": { + "selector": "string", + "strict": "boolean?", + "elements": {"type": "array?", "items": "ElementHandle"}, + "options": { + "type": "array?", + "items": { + "type": "object", + "properties": { + "valueOrLabel": "string?", + "value": "string?", + "label": "string?", + "index": "int?", + }, + }, + }, + "force": "boolean?", + }, + "setContent": {"html": "string", "waitUntil": "LifecycleEvent?"}, + "setInputFiles": { + "selector": "string", + "strict": "boolean?", + "payloads": { + "type": "array?", + "items": { + "type": "object", + "properties": { + "name": "string", + "mimeType": "string?", + "buffer": "binary", + }, + }, + }, + "localDirectory": "string?", + "directoryStream": "WritableStream?", + "localPaths": {"type": "array?", "items": "string"}, + "streams": {"type": "array?", "items": "WritableStream"}, + }, + "tap": { + "selector": "string", + "strict": "boolean?", + "force": "boolean?", + "scroll": {"type": "enum?", "literals": ["auto", "none"]}, + "modifiers": { + "type": "array?", + "items": { + "type": "enum", + "literals": ["Alt", "Control", "ControlOrMeta", "Meta", "Shift"], + }, + }, + "position": "Point?", + "trial": "boolean?", + }, + "textContent": {"selector": "string", "strict": "boolean?"}, + "title": {}, + "type": { + "selector": "string", + "strict": "boolean?", + "text": "string", + "delay": "float?", + }, + "uncheck": { + "selector": "string", + "strict": "boolean?", + "force": "boolean?", + "scroll": {"type": "enum?", "literals": ["auto", "none"]}, + "position": "Point?", + "trial": "boolean?", + }, + "waitForTimeout": {"waitTimeout": "float"}, + "waitForFunction": { + "expression": "string", + "isFunction": "boolean?", + "arg": "SerializedArgument", + "pollingInterval": "float?", + "selector": "string?", + "strict": "boolean?", + }, + "waitForSelector": { + "selector": "string", + "strict": "boolean?", + "state": { + "type": "enum?", + "literals": ["attached", "detached", "visible", "hidden"], + }, + "omitReturnValue": "boolean?", + }, + "expect": { + "selector": "string?", + "expression": "string", + "expressionArg": "json?", + "pseudo": {"type": "enum?", "literals": ["before", "after"]}, + "expectedText": {"type": "array?", "items": "ExpectedTextValue"}, + "expectedNumber": "float?", + "expectedValue": "SerializedArgument?", + "useInnerText": "boolean?", + "isNot": "boolean", + }, + "webmcpTools": {}, + "webmcpCallTool": {"name": "string", "input": "SerializedArgument"}, + }, + "JSHandle": { + "dispose": {}, + "evaluateExpression": { + "expression": "string", + "isFunction": "boolean?", + "arg": "SerializedArgument", + }, + "evaluateExpressionHandle": { + "expression": "string", + "isFunction": "boolean?", + "arg": "SerializedArgument", + }, + "getPropertyList": {}, + "getProperty": {"name": "string"}, + "jsonValue": {}, + }, + "ElementHandle": { + "evalOnSelector": { + "selector": "string", + "strict": "boolean?", + "expression": "string", + "isFunction": "boolean?", + "arg": "SerializedArgument", + }, + "evalOnSelectorAll": { + "selector": "string", + "expression": "string", + "isFunction": "boolean?", + "arg": "SerializedArgument", + }, + "boundingBox": {}, + "check": { + "force": "boolean?", + "scroll": {"type": "enum?", "literals": ["auto", "none"]}, + "position": "Point?", + "trial": "boolean?", + }, + "click": { + "force": "boolean?", + "scroll": {"type": "enum?", "literals": ["auto", "none"]}, + "noWaitAfter": "boolean?", + "modifiers": { + "type": "array?", + "items": { + "type": "enum", + "literals": ["Alt", "Control", "ControlOrMeta", "Meta", "Shift"], + }, + }, + "position": "Point?", + "delay": "float?", + "button": {"type": "enum?", "literals": ["left", "right", "middle"]}, + "clickCount": "int?", + "trial": "boolean?", + "steps": "int?", + }, + "contentFrame": {}, + "dblclick": { + "force": "boolean?", + "scroll": {"type": "enum?", "literals": ["auto", "none"]}, + "modifiers": { + "type": "array?", + "items": { + "type": "enum", + "literals": ["Alt", "Control", "ControlOrMeta", "Meta", "Shift"], + }, + }, + "position": "Point?", + "delay": "float?", + "button": {"type": "enum?", "literals": ["left", "right", "middle"]}, + "trial": "boolean?", + "steps": "int?", + }, + "dispatchEvent": {"type": "string", "eventInit": "SerializedArgument"}, + "fill": {"value": "string", "force": "boolean?"}, + "focus": {}, + "getAttribute": {"name": "string"}, + "hover": { + "force": "boolean?", + "scroll": {"type": "enum?", "literals": ["auto", "none"]}, + "modifiers": { + "type": "array?", + "items": { + "type": "enum", + "literals": ["Alt", "Control", "ControlOrMeta", "Meta", "Shift"], + }, + }, + "position": "Point?", + "trial": "boolean?", + }, + "innerHTML": {}, + "innerText": {}, + "inputValue": {}, + "isChecked": {}, + "isDisabled": {}, + "isEditable": {}, + "isEnabled": {}, + "isHidden": {}, + "isVisible": {}, + "ownerFrame": {}, + "press": {"key": "string", "delay": "float?", "noWaitAfter": "boolean?"}, + "querySelector": {"selector": "string", "strict": "boolean?"}, + "querySelectorAll": {"selector": "string"}, + "screenshot": { + "type": {"type": "enum?", "literals": ["png", "jpeg", "webp"]}, + "quality": "int?", + "omitBackground": "boolean?", + "caret": {"type": "enum?", "literals": ["hide", "initial"]}, + "animations": {"type": "enum?", "literals": ["disabled", "allow"]}, + "scale": {"type": "enum?", "literals": ["css", "device"]}, + "mask": { + "type": "array?", + "items": { + "type": "object", + "properties": {"frame": "Frame", "selector": "string"}, + }, + }, + "maskColor": "string?", + "style": "string?", + }, + "scrollIntoViewIfNeeded": {}, + "selectOption": { + "elements": {"type": "array?", "items": "ElementHandle"}, + "options": { + "type": "array?", + "items": { + "type": "object", + "properties": { + "valueOrLabel": "string?", + "value": "string?", + "label": "string?", + "index": "int?", + }, + }, + }, + "force": "boolean?", + }, + "selectText": {"force": "boolean?"}, + "setInputFiles": { + "payloads": { + "type": "array?", + "items": { + "type": "object", + "properties": { + "name": "string", + "mimeType": "string?", + "buffer": "binary", + }, + }, + }, + "localDirectory": "string?", + "directoryStream": "WritableStream?", + "localPaths": {"type": "array?", "items": "string"}, + "streams": {"type": "array?", "items": "WritableStream"}, + }, + "tap": { + "force": "boolean?", + "scroll": {"type": "enum?", "literals": ["auto", "none"]}, + "modifiers": { + "type": "array?", + "items": { + "type": "enum", + "literals": ["Alt", "Control", "ControlOrMeta", "Meta", "Shift"], + }, + }, + "position": "Point?", + "trial": "boolean?", + }, + "textContent": {}, + "type": {"text": "string", "delay": "float?"}, + "uncheck": { + "force": "boolean?", + "scroll": {"type": "enum?", "literals": ["auto", "none"]}, + "position": "Point?", + "trial": "boolean?", + }, + "waitForElementState": { + "state": { + "type": "enum", + "literals": [ + "visible", + "hidden", + "stable", + "enabled", + "disabled", + "editable", + ], + } + }, + "waitForSelector": { + "selector": "string", + "strict": "boolean?", + "state": { + "type": "enum?", + "literals": ["attached", "detached", "visible", "hidden"], + }, + }, + }, + "LocalUtils": { + "zip": { + "zipFile": "string", + "entries": {"type": "array", "items": "NameValue"}, + "stacksId": "string?", + "mode": {"type": "enum", "literals": ["write", "append"]}, + "includeSources": "boolean", + "additionalSources": {"type": "array?", "items": "string"}, + }, + "harOpen": {"file": "string"}, + "harLookup": { + "harId": "string", + "url": "string", + "method": "string", + "headers": {"type": "array", "items": "NameValue"}, + "postData": "binary?", + "isNavigationRequest": "boolean", + }, + "harClose": {"harId": "string"}, + "harUnzip": { + "zipFile": "string", + "harFile": "string", + "resourcesDir": "string?", + }, + "connect": { + "endpoint": "string", + "headers": "json?", + "exposeNetwork": "string?", + "slowMo": "float?", + "socksProxyRedirectPortForTest": "int?", + }, + "tracingStarted": { + "tracesDir": "string?", + "traceName": "string", + "live": "boolean?", + }, + "addStackToTracingNoReply": {"callData": "ClientSideCallMetadata"}, + "traceDiscarded": {"stacksId": "string"}, + "globToRegex": { + "glob": "string", + "baseURL": "string?", + "webSocketUrl": "boolean?", + }, + "asLocatorDescription": {"selector": "string", "sdkLanguage": "SDKLanguage"}, + }, + "Request": {"response": {}, "rawRequestHeaders": {}}, + "Route": { + "redirectNavigationRequest": {"url": "string"}, + "abort": {"errorCode": "string?"}, + "continue": { + "url": "string?", + "method": "string?", + "headers": {"type": "array?", "items": "NameValue"}, + "postData": "binary?", + "isFallback": "boolean", + }, + "fulfill": { + "status": "int?", + "headers": {"type": "array?", "items": "NameValue"}, + "body": "string?", + "isBase64": "boolean?", + "fetchResponseUid": "string?", + }, + }, + "WebSocketRoute": { + "connect": {}, + "ensureOpened": {}, + "sendToPage": {"message": "string", "isBase64": "boolean"}, + "sendToServer": {"message": "string", "isBase64": "boolean"}, + "closePage": {"code": "int?", "reason": "string?", "wasClean": "boolean"}, + "closeServer": {"code": "int?", "reason": "string?", "wasClean": "boolean"}, + }, + "Response": { + "body": {}, + "securityDetails": {}, + "serverAddr": {}, + "rawResponseHeaders": {}, + "httpVersion": {}, + "sizes": {}, + }, + "WebSocket": {}, + "Page": { + "addInitScript": {"source": "string"}, + "close": {"reason": "string?"}, + "runBeforeUnload": {}, + "clearConsoleMessages": {}, + "consoleMessages": {"filter": "ConsoleMessagesFilter?"}, + "emulateMedia": { + "media": {"type": "enum?", "literals": ["screen", "print", "no-override"]}, + "colorScheme": { + "type": "enum?", + "literals": ["dark", "light", "no-preference", "no-override"], + }, + "reducedMotion": { + "type": "enum?", + "literals": ["reduce", "no-preference", "no-override"], + }, + "forcedColors": { + "type": "enum?", + "literals": ["active", "none", "no-override"], + }, + "contrast": { + "type": "enum?", + "literals": ["no-preference", "more", "no-override"], + }, + }, + "exposeBinding": {"name": "string", "noGlobal": "boolean?"}, + "goBack": {"waitUntil": "LifecycleEvent?"}, + "goForward": {"waitUntil": "LifecycleEvent?"}, + "requestGC": {}, + "registerLocatorHandler": {"selector": "string", "noWaitAfter": "boolean?"}, + "resolveLocatorHandlerNoReply": {"uid": "int", "remove": "boolean?"}, + "unregisterLocatorHandler": {"uid": "int"}, + "reload": {"waitUntil": "LifecycleEvent?"}, + "expectScreenshot": { + "expected": "binary?", + "isNot": "boolean", + "locator": { + "type": "object?", + "properties": {"frame": "Frame", "selector": "string"}, + }, + "comparator": "string?", + "maxDiffPixels": "int?", + "maxDiffPixelRatio": "float?", + "threshold": "float?", + "fullPage": "boolean?", + "clip": "Rect?", + "type": {"type": "enum?", "literals": ["png", "webp"]}, + "omitBackground": "boolean?", + "caret": {"type": "enum?", "literals": ["hide", "initial"]}, + "animations": {"type": "enum?", "literals": ["disabled", "allow"]}, + "scale": {"type": "enum?", "literals": ["css", "device"]}, + "mask": { + "type": "array?", + "items": { + "type": "object", + "properties": {"frame": "Frame", "selector": "string"}, + }, + }, + "maskColor": "string?", + "style": "string?", + }, + "screenshot": { + "type": {"type": "enum?", "literals": ["png", "jpeg", "webp"]}, + "quality": "int?", + "fullPage": "boolean?", + "clip": "Rect?", + "omitBackground": "boolean?", + "caret": {"type": "enum?", "literals": ["hide", "initial"]}, + "animations": {"type": "enum?", "literals": ["disabled", "allow"]}, + "scale": {"type": "enum?", "literals": ["css", "device"]}, + "mask": { + "type": "array?", + "items": { + "type": "object", + "properties": {"frame": "Frame", "selector": "string"}, + }, + }, + "maskColor": "string?", + "style": "string?", + }, + "setExtraHTTPHeaders": {"headers": {"type": "array", "items": "NameValue"}}, + "setNetworkInterceptionPatterns": { + "patterns": { + "type": "array", + "items": { + "type": "object", + "properties": { + "glob": "string?", + "regexSource": "string?", + "regexFlags": "string?", + "urlPattern": "URLPattern?", + }, + }, + } + }, + "setWebSocketInterceptionPatterns": { + "patterns": { + "type": "array", + "items": { + "type": "object", + "properties": { + "glob": "string?", + "regexSource": "string?", + "regexFlags": "string?", + "urlPattern": "URLPattern?", + }, + }, + } + }, + "setViewportSize": { + "viewportSize": { + "type": "object", + "properties": {"width": "int", "height": "int"}, + } + }, + "keyboardDown": {"key": "string"}, + "keyboardUp": {"key": "string"}, + "keyboardInsertText": {"text": "string"}, + "keyboardType": {"text": "string", "delay": "float?"}, + "keyboardPress": {"key": "string", "delay": "float?"}, + "mouseMove": {"x": "float", "y": "float", "steps": "int?"}, + "mouseDown": { + "button": {"type": "enum?", "literals": ["left", "right", "middle"]}, + "clickCount": "int?", + }, + "mouseUp": { + "button": {"type": "enum?", "literals": ["left", "right", "middle"]}, + "clickCount": "int?", + }, + "mouseClick": { + "x": "float", + "y": "float", + "delay": "float?", + "button": {"type": "enum?", "literals": ["left", "right", "middle"]}, + "clickCount": "int?", + }, + "mouseWheel": {"deltaX": "float", "deltaY": "float"}, + "touchscreenTap": {"x": "float", "y": "float"}, + "clearPageErrors": {}, + "pageErrors": {"filter": "ConsoleMessagesFilter?"}, + "pdf": { + "scale": "float?", + "displayHeaderFooter": "boolean?", + "headerTemplate": "string?", + "footerTemplate": "string?", + "printBackground": "boolean?", + "landscape": "boolean?", + "pageRanges": "string?", + "format": "string?", + "width": "string?", + "height": "string?", + "preferCSSPageSize": "boolean?", + "margin": { + "type": "object?", + "properties": { + "top": "string?", + "bottom": "string?", + "left": "string?", + "right": "string?", + }, + }, + "tagged": "boolean?", + "outline": "boolean?", + }, + "requests": {}, + "startJSCoverage": { + "resetOnNavigation": "boolean?", + "reportAnonymousScripts": "boolean?", + }, + "stopJSCoverage": {}, + "startCSSCoverage": {"resetOnNavigation": "boolean?"}, + "stopCSSCoverage": {}, + "bringToFront": {}, + "pickLocator": {}, + "cancelPickLocator": {}, + "hideHighlight": {}, + "screencastShowOverlay": {"html": "string", "duration": "float?"}, + "screencastRemoveOverlay": {"id": "string"}, + "screencastChapter": { + "title": "string", + "description": "string?", + "duration": "float?", + }, + "screencastSetOverlayVisible": {"visible": "boolean"}, + "screencastShowActions": { + "duration": "float?", + "position": { + "type": "enum?", + "literals": [ + "top-left", + "top", + "top-right", + "bottom-left", + "bottom", + "bottom-right", + ], + }, + "fontSize": "int?", + "cursor": {"type": "enum?", "literals": ["none", "pointer"]}, + "style": { + "type": "object?", + "properties": { + "point": "string?", + "highlight": "string?", + "title": "string?", + }, + }, + }, + "screencastHideActions": {}, + "screencastStart": { + "size": { + "type": "object?", + "properties": {"width": "int", "height": "int"}, + }, + "quality": "int?", + "fps": "int?", + "sendFrames": "boolean?", + "record": "boolean?", + }, + "screencastFrameAck": {"frameId": "int"}, + "screencastStop": {}, + "updateSubscription": { + "event": { + "type": "enum", + "literals": [ + "console", + "dialog", + "dialogClosed", + "fileChooser", + "request", + "response", + "requestFinished", + "requestFailed", + ], + }, + "enabled": "boolean", + }, + "setDockTile": {"image": "binary"}, + "webStorageItems": {"kind": {"type": "enum", "literals": ["local", "session"]}}, + "webStorageGetItem": { + "kind": {"type": "enum", "literals": ["local", "session"]}, + "name": "string", + }, + "webStorageSetItem": { + "kind": {"type": "enum", "literals": ["local", "session"]}, + "name": "string", + "value": "string", + }, + "webStorageRemoveItem": { + "kind": {"type": "enum", "literals": ["local", "session"]}, + "name": "string", + }, + "webStorageClear": {"kind": {"type": "enum", "literals": ["local", "session"]}}, + }, + "Root": {"initialize": {"sdkLanguage": "SDKLanguage"}}, + "Playwright": { + "newRequest": { + "baseURL": "string?", + "userAgent": "string?", + "ignoreHTTPSErrors": "boolean?", + "extraHTTPHeaders": {"type": "array?", "items": "NameValue"}, + "failOnStatusCode": "boolean?", + "clientCertificates": { + "type": "array?", + "items": { + "type": "object", + "properties": { + "origin": "string", + "cert": "binary?", + "key": "binary?", + "passphrase": "string?", + "pfx": "binary?", + }, + }, + }, + "maxRedirects": "int?", + "httpCredentials": {"type": "array?", "items": "HttpCredentials"}, + "proxy": { + "type": "object?", + "properties": { + "server": "string", + "bypass": "string?", + "username": "string?", + "password": "string?", + }, + }, + "storageState": { + "type": "object?", + "properties": { + "cookies": {"type": "array?", "items": "NetworkCookie"}, + "origins": {"type": "array?", "items": "SetOriginStorage"}, + }, + }, + "tracesDir": "string?", + } + }, + "DebugController": { + "initialize": {"codegenId": "string", "sdkLanguage": "SDKLanguage"}, + "setReportStateChanged": {"enabled": "boolean"}, + "setRecorderMode": { + "mode": {"type": "enum", "literals": ["inspecting", "recording", "none"]}, + "testIdAttributeName": "string?", + "generateAutoExpect": "boolean?", + }, + "highlight": {"selector": "string?", "ariaTemplate": "string?"}, + "hideHighlight": {}, + "resume": {}, + "kill": {}, + }, + "SocksSupport": { + "socksConnected": {"uid": "string", "host": "string", "port": "int"}, + "socksFailed": {"uid": "string", "errorCode": "string"}, + "socksData": {"uid": "string", "data": "binary"}, + "socksError": {"uid": "string", "error": "string"}, + "socksEnd": {"uid": "string"}, + }, + "JsonPipe": {"send": {"message": "json"}, "close": {}}, + "CDPSession": {"send": {"method": "string", "params": "json?"}, "detach": {}}, + "BindingCall": { + "reject": {"error": "SerializedError"}, + "resolve": {"result": "SerializedArgument"}, + }, + "Debugger": { + "requestPause": {}, + "resume": {}, + "next": {}, + "runTo": { + "location": { + "type": "object", + "properties": {"file": "string", "line": "int?", "column": "int?"}, + } + }, + "enable": {}, + }, + "Dialog": {"accept": {"promptText": "string?"}, "dismiss": {}}, + "Tracing": { + "tracingStart": { + "name": "string?", + "snapshotDom": "boolean?", + "snapshotAria": "boolean?", + "snapshotScreen": "boolean?", + "screencast": "boolean?", + "coverage": "boolean?", + "live": "boolean?", + }, + "tracingStartChunk": {"name": "string?", "title": "string?"}, + "tracingGroup": { + "name": "string", + "location": { + "type": "object?", + "properties": {"file": "string", "line": "int?", "column": "int?"}, + }, + }, + "tracingGroupEnd": {}, + "tracingStopChunk": { + "mode": {"type": "enum", "literals": ["archive", "discard", "entries"]} + }, + "tracingStop": {}, + "harStart": {"page": "Page?", "options": "RecordHarOptions"}, + "harExport": { + "harId": "string?", + "mode": {"type": "enum", "literals": ["archive", "entries"]}, + }, + }, + "Worker": { + "disconnect": {"reason": "string?"}, + "evaluateExpression": { + "expression": "string", + "isFunction": "boolean?", + "arg": "SerializedArgument", + }, + "evaluateExpressionHandle": { + "expression": "string", + "isFunction": "boolean?", + "arg": "SerializedArgument", + }, + "updateSubscription": { + "event": {"type": "enum", "literals": ["console"]}, + "enabled": "boolean", + }, + }, +} diff --git a/playwright/_impl/_protocol_check.py b/playwright/_impl/_protocol_check.py new file mode 100644 index 000000000..04aa5c959 --- /dev/null +++ b/playwright/_impl/_protocol_check.py @@ -0,0 +1,159 @@ +# 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. + +# Checks each message that the client sends against the protocol spec +# (_protocol.py). The driver drops parameters that it does not know without an +# error, so a wrong parameter name in the client is silent without this check. +# +# The check is on when PW_CHECK_PROTOCOL=1; the test suite sets it. It does not +# raise: it records each violation, and the test suite fails the test that +# caused it. So a violation in a message that is sent without waiting for the +# reply is reported as well. + +import os +from typing import Any, Callable, Dict, List, Optional + +from playwright._impl._protocol import COMMANDS, EXTENDS, TYPES + +_violations: List[str] = [] + + +def is_enabled() -> bool: + return os.environ.get("PW_CHECK_PROTOCOL") == "1" + + +def take_violations() -> List[str]: + result = list(_violations) + _violations.clear() + return result + + +def check_message( + type_name: str, + method: str, + params: Dict, + metadata: Dict, + type_of_guid: Callable[[str], Optional[str]], +) -> None: + checker = _Checker(type_of_guid) + if method == "__waitInfo__": + checker.check("WaitInfo", params, "params") + else: + properties = _find_command(type_name, method) + if properties is None: + checker.errors.append("unknown method") + else: + checker.check_object(properties, params, "params") + checker.check("Metadata", metadata, "metadata") + for error in checker.errors: + _violations.append(f"{type_name}.{method}: {error}") + + +def _find_command(type_name: Optional[str], method: str) -> Optional[Dict[str, Any]]: + while type_name: + commands = COMMANDS.get(type_name, {}) + if method in commands: + return commands[method] + type_name = EXTENDS.get(type_name) + return None + + +def _is_same_or_derived(type_name: Optional[str], base: str) -> bool: + while type_name: + if type_name == base: + return True + type_name = EXTENDS.get(type_name) + return False + + +class _Checker: + def __init__(self, type_of_guid: Callable[[str], Optional[str]]) -> None: + self._type_of_guid = type_of_guid + self.errors: List[str] = [] + + def check(self, spec: Any, value: Any, path: str) -> None: + if isinstance(spec, str): + self._check_name(spec.rstrip("?"), value, path) + return + kind = spec["type"].rstrip("?") + if kind == "array": + if not isinstance(value, list): + self._expected("array", value, path) + return + for index, item in enumerate(value): + self.check(spec["items"], item, f"{path}[{index}]") + elif kind == "enum": + if value not in spec["literals"]: + literals = "|".join(spec["literals"]) + self.errors.append( + f"{path}: expected one of ({literals}), got {value!r}" + ) + elif kind == "object": + self.check_object(spec["properties"], value, path) + + def check_object(self, properties: Dict[str, Any], value: Any, path: str) -> None: + if not isinstance(value, dict): + self._expected("object", value, path) + return + for name, spec in properties.items(): + if name in value: + self.check(spec, value[name], f"{path}.{name}") + elif not _is_optional(spec): + self.errors.append(f"{path}.{name}: missing") + for name in value: + if name not in properties: + self.errors.append(f"{path}.{name}: unknown") + + def _check_name(self, name: str, value: Any, path: str) -> None: + if name == "json": + return + if name in ("string", "binary"): + if not isinstance(value, str): + self._expected("string", value, path) + elif name == "int": + if isinstance(value, bool) or not ( + isinstance(value, int) + or (isinstance(value, float) and value.is_integer()) + ): + self._expected("integer", value, path) + elif name == "float": + if isinstance(value, bool) or not isinstance(value, (int, float)): + self._expected("float", value, path) + elif name == "boolean": + if not isinstance(value, bool): + self._expected("boolean", value, path) + elif name == "Channel" or name in COMMANDS: + self._check_channel(name, value, path) + else: + self.check(TYPES[name], value, path) + + def _check_channel(self, name: str, value: Any, path: str) -> None: + if not isinstance(value, dict) or not isinstance(value.get("guid"), str): + self._expected(name, value, path) + return + if name == "Channel": + return + actual = self._type_of_guid(value["guid"]) + # An unknown guid is an object that is already gone; the driver reports it. + if actual and not _is_same_or_derived(actual, name): + self.errors.append(f"{path}: expected {name}, got {actual}") + + def _expected(self, expected: str, value: Any, path: str) -> None: + self.errors.append(f"{path}: expected {expected}, got {type(value).__name__}") + + +def _is_optional(spec: Any) -> bool: + if isinstance(spec, str): + return spec.endswith("?") + return spec["type"].endswith("?") diff --git a/playwright/_impl/_url_match.py b/playwright/_impl/_url_match.py new file mode 100644 index 000000000..463e9b701 --- /dev/null +++ b/playwright/_impl/_url_match.py @@ -0,0 +1,198 @@ +# 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 re +from typing import Callable, Dict, Optional, Pattern, Tuple, Union +from urllib.parse import ParseResult, urljoin, urlparse, urlunparse + +URLMatch = Union[str, Pattern[str], Callable[[str], bool]] + +# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping +escaped_chars = {"$", "^", "+", ".", "*", "(", ")", "|", "\\", "?", "{", "}", "[", "]"} + + +def glob_to_regex_pattern(glob: str) -> str: + tokens = ["^"] + in_group = False + + i = 0 + while i < len(glob): + c = glob[i] + if c == "\\" and i + 1 < len(glob): + char = glob[i + 1] + tokens.append("\\" + char if char in escaped_chars else char) + i += 1 + elif c == "*": + char_before = glob[i - 1] if i > 0 else None + star_count = 1 + while i + 1 < len(glob) and glob[i + 1] == "*": + star_count += 1 + i += 1 + if star_count > 1: + char_after = glob[i + 1] if i + 1 < len(glob) else None + if char_after == "/": + if char_before == "/": + tokens.append("((.+/)|)") + else: + tokens.append("(.*/)") + i += 1 + else: + tokens.append("(.*)") + else: + tokens.append("([^/]*)") + else: + if c == "{": + in_group = True + tokens.append("(") + elif c == "}": + in_group = False + tokens.append(")") + elif c == ",": + if in_group: + tokens.append("|") + else: + tokens.append("\\" + c) + else: + tokens.append("\\" + c if c in escaped_chars else c) + i += 1 + + tokens.append("$") + return "".join(tokens) + + +def url_matches( + base_url: Optional[str], + url_string: str, + match: Optional[URLMatch], + websocket_url: bool = None, +) -> bool: + if not match: + return True + if isinstance(match, str): + match = re.compile( + resolve_glob_to_regex_pattern(base_url, match, websocket_url) + ) + if isinstance(match, Pattern): + return bool(match.search(url_string)) + return match(url_string) + + +def resolve_glob_to_regex_pattern( + base_url: Optional[str], glob: str, websocket_url: bool = None +) -> str: + if websocket_url: + base_url = to_websocket_base_url(base_url) + glob = resolve_glob_base(base_url, glob) + return glob_to_regex_pattern(glob) + + +def to_websocket_base_url(base_url: Optional[str]) -> Optional[str]: + if base_url is not None and re.match(r"^https?://", base_url): + base_url = re.sub(r"^http", "ws", base_url) + return base_url + + +def resolve_glob_base(base_url: Optional[str], match: str) -> str: + if match[0] == "*": + return match + + token_map: Dict[str, str] = {} + + def map_token(original: str, replacement: str) -> str: + if len(original) == 0: + return "" + token_map[replacement] = original + return replacement + + # Escaped `\\?` behaves the same as `?` in our glob patterns. + match = match.replace(r"\\?", "?") + # Special case about: URLs as they are not relative to base_url + if ( + match.startswith("about:") + or match.startswith("data:") + or match.startswith("chrome:") + or match.startswith("edge:") + or match.startswith("file:") + ): + # about: and data: URLs are not relative to base_url, so we return them as is. + return match + # Glob symbols may be escaped in the URL and some of them such as ? affect resolution, + # so we replace them with safe components first. + processed_parts = [] + for index, token in enumerate(match.split("/")): + if token in (".", "..", ""): + processed_parts.append(token) + continue + # Handle special case of http*://, note that the new schema has to be + # a web schema so that slashes are properly inserted after domain. + if index == 0 and token.endswith(":"): + # Replace any pattern with http: + if "*" in token or "{" in token: + processed_parts.append(map_token(token, "http:")) + else: + # Preserve explicit schema as is as it may affect trailing slashes after domain. + processed_parts.append(token) + continue + question_index = token.find("?") + if question_index == -1: + processed_parts.append(map_token(token, f"$_{index}_$")) + else: + new_prefix = map_token(token[:question_index], f"$_{index}_$") + new_suffix = map_token(token[question_index:], f"?$_{index}_$") + processed_parts.append(new_prefix + new_suffix) + + relative_path = "/".join(processed_parts) + resolved, case_insensitive_part = resolve_base_url(base_url, relative_path) + + for token, original in token_map.items(): + normalize = case_insensitive_part and token in case_insensitive_part + resolved = resolved.replace( + token, original.lower() if normalize else original, 1 + ) + + return resolved + + +def resolve_base_url( + base_url: Optional[str], given_url: str +) -> Tuple[str, Optional[str]]: + try: + url = nodelike_urlparse( + urljoin(base_url if base_url is not None else "", given_url) + ) + resolved = urlunparse(url) + # Schema and domain are case-insensitive. + hostname_port = ( + url.hostname or "" + ) # can't use parsed.netloc because it includes userinfo (username:password) + if url.port: + hostname_port += f":{url.port}" + case_insensitive_prefix = f"{url.scheme}://{hostname_port}" + return resolved, case_insensitive_prefix + except Exception: + return given_url, None + + +def nodelike_urlparse(url: str) -> ParseResult: + parsed = urlparse(url, allow_fragments=True) + + # https://url.spec.whatwg.org/#special-scheme + is_special_url = parsed.scheme in ["http", "https", "ws", "wss", "ftp", "file"] + if is_special_url: + # special urls have a list path, list paths are serialized as follows: https://url.spec.whatwg.org/#url-path-serializer + # urllib diverges, so we patch it here + if parsed.path == "": + parsed = parsed._replace(path="/") + + return parsed diff --git a/pyproject.toml b/pyproject.toml index 0e5e5db84..14759da68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,8 @@ markers = [ "skip_browser", "only_browser", "skip_platform", - "only_platform" + "only_platform", + "skip_protocol_check" ] junit_family = "xunit2" asyncio_mode = "auto" diff --git a/scripts/codegen/generate_protocol.py b/scripts/codegen/generate_protocol.py new file mode 100644 index 000000000..17e51b779 --- /dev/null +++ b/scripts/codegen/generate_protocol.py @@ -0,0 +1,177 @@ +# 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. + +# Generates playwright/_impl/_protocol.py from the protocol spec +# (packages/protocol/spec/*.yml in microsoft/playwright). +# +# Usage: +# PW_SRC_DIR= python scripts/codegen/generate_protocol.py [--out ] +# +# The spec is loaded the same way as utils/protocol_spec.js does it. The output +# is not formatted; run black on it. + +import argparse +import json +import os +import sys +from pathlib import Path +from typing import Any, Dict, List + +import yaml + +PRIMITIVES = {"string", "int", "float", "boolean", "binary", "json", "Channel"} + +errors: List[str] = [] + + +def load_protocol(spec_dir: Path) -> Dict[str, Any]: + protocol: Dict[str, Any] = {} + for file in sorted(spec_dir.glob("*.yml")): + doc = yaml.safe_load(file.read_text(encoding="utf-8")) + if not isinstance(doc, dict): + raise SystemExit(f"{file} is empty or not a YAML map") + for name, value in doc.items(): + if name in protocol: + raise SystemExit(f'Duplicate top-level definition "{name}" in {file}') + protocol[name] = value + return protocol + + +def js_string(value: Any) -> str: + if value is None: + return "null" + if isinstance(value, bool): + return "true" if value else "false" + return str(value) + + +class Converter: + def __init__(self, protocol: Dict[str, Any]) -> None: + self._protocol = protocol + + def properties(self, props: Any, path: str) -> Dict[str, Any]: + result: Dict[str, Any] = {} + for name, value in (props or {}).items(): + if name.startswith("$mixin"): + mixin = self._protocol[value] + result.update(self.properties(mixin.get("properties"), path)) + continue + if not isinstance(name, str): + errors.append(f"{path}: property name {name!r} is not a string") + continue + result[name] = self.type(value, f"{path}.{name}") + return result + + def type(self, value: Any, path: str) -> Any: + if isinstance(value, str): + name = value.rstrip("?") + if name not in PRIMITIVES and name not in self._protocol: + errors.append(f"{path}: unknown type {name}") + return value + kind = value["type"].rstrip("?") + if kind == "array": + return {"type": value["type"], "items": self.type(value["items"], path)} + if kind == "enum": + # Upstream turns each literal into a string, so `- null` is "null". + return { + "type": value["type"], + "literals": [js_string(literal) for literal in value["literals"]], + } + if kind == "object": + return { + "type": value["type"], + "properties": self.properties(value.get("properties"), path), + } + errors.append(f"{path}: unknown type {value['type']}") + return value + + +def generate(protocol: Dict[str, Any]) -> str: + converter = Converter(protocol) + extends: Dict[str, str] = {} + types: Dict[str, Any] = {} + commands: Dict[str, Dict[str, Any]] = {} + for name, item in protocol.items(): + if item["type"] == "interface": + if item.get("extends"): + extends[name] = item["extends"] + commands[name] = {} + for method, command in (item.get("commands") or {}).items(): + commands[name][method] = converter.properties( + (command or {}).get("parameters"), f"{name}.{method}" + ) + elif item["type"] in ("object", "enum"): + types[name] = converter.type(item, name) + + def dump(value: Any) -> str: + return json.dumps(value, indent=4, sort_keys=False) + + return "\n".join( + [ + LICENSE, + "# Generated from packages/protocol/spec by scripts/codegen/generate_protocol.py. Do not edit.", + "", + "from typing import Any, Dict", + "", + "# The interface that each interface extends.", + f"EXTENDS: Dict[str, str] = {dump(extends)}", + "", + "# The object and enum types, in the form of the spec.", + f"TYPES: Dict[str, Any] = {dump(types)}", + "", + "# The parameters of each command, by interface and method.", + f"COMMANDS: Dict[str, Dict[str, Dict[str, Any]]] = {dump(commands)}", + "", + ] + ) + + +LICENSE = """# 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. +""" + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--out") + args = parser.parse_args() + pw_src_dir = os.environ.get("PW_SRC_DIR") + if not pw_src_dir: + raise SystemExit( + "Set PW_SRC_DIR to a microsoft/playwright checkout at the release branch in DRIVER_VERSION." + ) + text = generate(load_protocol(Path(pw_src_dir) / "packages" / "protocol" / "spec")) + if errors: + for error in errors: + print(error, file=sys.stderr) + raise SystemExit(1) + if args.out: + Path(args.out).write_text(text, encoding="utf-8") + else: + sys.stdout.write(text) + + +if __name__ == "__main__": + main() diff --git a/scripts/update_api.sh b/scripts/update_api.sh index 5668b56cf..87ba909b8 100755 --- a/scripts/update_api.sh +++ b/scripts/update_api.sh @@ -2,7 +2,9 @@ # Generates the public API (playwright/{async,sync}_api/_generated.py) and its # TypedDicts (playwright/_impl/_api_structures.py) from the Playwright API docs -# (docs/src/api) in a microsoft/playwright checkout, pointed to by PW_SRC_DIR. +# (docs/src/api) in a microsoft/playwright checkout, pointed to by PW_SRC_DIR, +# and the protocol tables (playwright/_impl/_protocol.py) from its protocol spec +# (packages/protocol/spec). # Use a separate checkout on the exact release branch for the version in # DRIVER_VERSION (for example release-1.64). The generator is # scripts/codegen/generate_api.js; Python facts that the docs do not contain @@ -32,6 +34,14 @@ update_api structs playwright/_impl/_api_structures.py update_api sync update_api async +echo "Generating playwright/_impl/_protocol.py" +if python scripts/codegen/generate_protocol.py --out playwright/_impl/_protocol.py; then + pre-commit run --files playwright/_impl/_protocol.py +else + echo "Exited due to errors" + exit 1 +fi + playwright install python scripts/update_versions.py diff --git a/tests/async/test_jshandle.py b/tests/async/test_jshandle.py index f18cbd633..e319942ab 100644 --- a/tests/async/test_jshandle.py +++ b/tests/async/test_jshandle.py @@ -23,9 +23,7 @@ async def test_jshandle_evaluate_work(page: Page) -> None: window_handle = await page.evaluate_handle("window") assert window_handle - assert ( - repr(window_handle) == f"" - ) + assert repr(window_handle) == f"" async def test_jshandle_evaluate_accept_object_handle_as_argument(page: Page) -> None: diff --git a/tests/async/test_navigation.py b/tests/async/test_navigation.py index 505940dbe..a0e12a9fa 100644 --- a/tests/async/test_navigation.py +++ b/tests/async/test_navigation.py @@ -245,6 +245,7 @@ async def test_goto_should_not_crash_when_navigating_to_bad_ssl_after_a_cross_or await page.goto(https_server.EMPTY_PAGE) +@pytest.mark.skip_protocol_check async def test_goto_should_throw_if_networkidle2_is_passed_as_an_option( page: Page, server: Server ) -> None: diff --git a/tests/async/test_network.py b/tests/async/test_network.py index b03490ef6..b5b3f03c2 100644 --- a/tests/async/test_network.py +++ b/tests/async/test_network.py @@ -880,6 +880,7 @@ async def test_set_extra_http_headers_should_override_extra_headers_from_browser assert request.getHeader("bar") == "foO" +@pytest.mark.skip_protocol_check async def test_set_extra_http_headers_should_throw_for_non_string_header_values( page: Page, ) -> None: diff --git a/tests/async/test_page.py b/tests/async/test_page.py index 172fe1ea2..9022cf16e 100644 --- a/tests/async/test_page.py +++ b/tests/async/test_page.py @@ -1050,6 +1050,7 @@ async def test_fill_should_throw_when_element_is_not_an_input_textarea_or_conten assert "Element is not an " in exc_info.value.message +@pytest.mark.skip_protocol_check async def test_fill_should_throw_if_passed_a_non_string_value( page: Page, server: Server ) -> None: diff --git a/tests/async/test_page_event_request.py b/tests/async/test_page_event_request.py index 73d6fd785..34881977f 100644 --- a/tests/async/test_page_event_request.py +++ b/tests/async/test_page_event_request.py @@ -14,8 +14,7 @@ import asyncio -from playwright._impl._network import Request, Route -from playwright._impl._page import Page +from playwright.async_api import Page, Request, Route from tests.server import Server diff --git a/tests/async/test_page_route.py b/tests/async/test_page_route.py index ecfac7f73..0d3d848cd 100644 --- a/tests/async/test_page_route.py +++ b/tests/async/test_page_route.py @@ -20,8 +20,7 @@ import pytest -from playwright._impl._glob import glob_to_regex_pattern -from playwright._impl._helper import url_matches +from playwright._impl._url_match import glob_to_regex_pattern, url_matches from playwright.async_api import ( Browser, BrowserContext, diff --git a/tests/async/test_page_select_option.py b/tests/async/test_page_select_option.py index c5edf543d..5275d478c 100644 --- a/tests/async/test_page_select_option.py +++ b/tests/async/test_page_select_option.py @@ -205,6 +205,7 @@ async def test_select_option_should_return_on_no_values( assert result == [] +@pytest.mark.skip_protocol_check async def test_select_option_should_not_allow_null_items( page: Page, server: Server ) -> None: diff --git a/tests/async/test_proxy.py b/tests/async/test_proxy.py index 694786aae..ae948a83f 100644 --- a/tests/async/test_proxy.py +++ b/tests/async/test_proxy.py @@ -22,6 +22,7 @@ from tests.server import Server, TestServerRequest +@pytest.mark.skip_protocol_check async def test_should_throw_for_bad_server_value( browser_factory: "Callable[..., asyncio.Future[Browser]]", ) -> None: diff --git a/tests/async/test_route_web_socket.py b/tests/async/test_route_web_socket.py index da6ce8757..823695394 100644 --- a/tests/async/test_route_web_socket.py +++ b/tests/async/test_route_web_socket.py @@ -16,8 +16,7 @@ import re from typing import Any, Awaitable, Callable, Literal, Tuple, Union -from playwright.async_api import Frame, Page, WebSocketRoute -from playwright.async_api._generated import Browser +from playwright.async_api import Browser, Frame, Page, WebSocketRoute from tests.server import Server, WebSocketProtocol diff --git a/tests/async/test_selectors_misc.py b/tests/async/test_selectors_misc.py index 5ad6c3519..198795b70 100644 --- a/tests/async/test_selectors_misc.py +++ b/tests/async/test_selectors_misc.py @@ -14,10 +14,7 @@ import pytest -from playwright._impl._browser import Browser -from playwright._impl._errors import Error -from playwright._impl._selectors import Selectors -from playwright.async_api import Page +from playwright.async_api import Browser, Error, Page, Selectors async def test_should_work_with_internal_and(page: Page) -> None: diff --git a/tests/conftest.py b/tests/conftest.py index d12a065c0..3cf74d630 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -29,11 +29,15 @@ import playwright from playwright._impl._path_utils import get_file_dirname +from playwright._impl._protocol_check import take_violations from .server import Server, test_server _dirname = get_file_dirname() +# Check each message that the client sends against the protocol spec. +os.environ.setdefault("PW_CHECK_PROTOCOL", "1") + def pytest_configure(config: pytest.Config) -> None: if os.environ.get("CI"): @@ -89,6 +93,16 @@ def after_each_hook() -> Generator[None, None, None]: test_server.reset() +@pytest.fixture(autouse=True) +def check_protocol(request: pytest.FixtureRequest) -> Generator[None, None, None]: + yield + violations = take_violations() + # The test sends an invalid message on purpose, to test the driver's error. + if request.node.get_closest_marker("skip_protocol_check"): + return + assert not violations, "Protocol violations:\n" + "\n".join(violations) + + @pytest.fixture(scope="session") def browser_name(pytestconfig: pytest.Config) -> str: return cast(str, pytestconfig.getoption("browser")) diff --git a/tests/sync/test_sync.py b/tests/sync/test_sync.py index 122c03cff..22832517e 100644 --- a/tests/sync/test_sync.py +++ b/tests/sync/test_sync.py @@ -74,7 +74,7 @@ def test_browser_context_repr(context: BrowserContext) -> None: def test_browser_repr(browser: Browser) -> None: assert ( repr(browser) - == f"" + == f"" )