diff --git a/.azure-pipelines/publish.yml b/.azure-pipelines/publish.yml index d0d308342..1a2e0e7a4 100644 --- a/.azure-pipelines/publish.yml +++ b/.azure-pipelines/publish.yml @@ -74,8 +74,8 @@ extends: # contentsource: 'folder' folderlocation: '$(Build.ArtifactStagingDirectory)/esrp-build' waitforreleasecompletion: true - owners: 'maxschmitt@microsoft.com' - approvers: 'maxschmitt@microsoft.com' + owners: 'yurys@microsoft.com' + approvers: 'yurys@microsoft.com' serviceendpointurl: 'https://api.esrp.microsoft.com' mainpublisher: 'Playwright' domaintenantid: '975f013f-7f24-47e8-a7d3-abc4752bf346' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a6d8fcd5..65ba1f433 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,9 +21,9 @@ jobs: name: Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.10" - name: Install dependencies & browsers @@ -80,9 +80,9 @@ jobs: browser: chromium runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies & browsers @@ -127,9 +127,9 @@ jobs: browser-channel: msedge runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.10" - name: Install dependencies & browsers @@ -160,10 +160,10 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-22.04, macos-13, windows-2019] + os: [ubuntu-22.04, macos-13, windows-2022] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Get conda @@ -184,9 +184,9 @@ jobs: run: working-directory: examples/todomvc/ steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.10' - name: Install dependencies & browsers diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b682372fd..c6e71028a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -25,7 +25,7 @@ jobs: # Required for conda-incubator/setup-miniconda@v3 shell: bash -el {0} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Get conda diff --git a/.github/workflows/publish_docker.yml b/.github/workflows/publish_docker.yml index 7d83136bc..7494f1abc 100644 --- a/.github/workflows/publish_docker.yml +++ b/.github/workflows/publish_docker.yml @@ -15,7 +15,7 @@ jobs: contents: read # This is required for actions/checkout to succeed environment: Docker steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Azure login uses: azure/login@v2 with: @@ -25,7 +25,7 @@ jobs: - name: Login to ACR via OIDC run: az acr login --name playwright - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.10" - name: Set up Docker QEMU for arm64 docker builds diff --git a/.github/workflows/test_docker.yml b/.github/workflows/test_docker.yml index 7f0ca3088..464eb3b46 100644 --- a/.github/workflows/test_docker.yml +++ b/.github/workflows/test_docker.yml @@ -19,17 +19,20 @@ on: jobs: build: timeout-minutes: 120 - runs-on: ubuntu-24.04 + runs-on: ${{ matrix.runs-on }} strategy: fail-fast: false matrix: docker-image-variant: - jammy - noble + runs-on: + - ubuntu-24.04 + - ubuntu-24.04-arm steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.10" - name: Install dependencies @@ -39,10 +42,12 @@ jobs: pip install -r requirements.txt pip install -e . - name: Build Docker image - run: bash utils/docker/build.sh --amd64 ${{ matrix.docker-image-variant }} playwright-python:localbuild-${{ matrix.docker-image-variant }} + run: | + ARCH="${{ matrix.runs-on == 'ubuntu-24.04-arm' && 'arm64' || 'amd64' }}" + bash utils/docker/build.sh --$ARCH ${{ matrix.docker-image-variant }} playwright-python:localbuild-${{ matrix.docker-image-variant }} - name: Test run: | - CONTAINER_ID="$(docker run --rm -v $(pwd):/root/playwright --name playwright-docker-test --workdir /root/playwright/ -d -t playwright-python:localbuild-${{ matrix.docker-image-variant }} /bin/bash)" + CONTAINER_ID="$(docker run --rm -e CI -v $(pwd):/root/playwright --name playwright-docker-test --workdir /root/playwright/ -d -t playwright-python:localbuild-${{ matrix.docker-image-variant }} /bin/bash)" # Fix permissions for Git inside the container docker exec "${CONTAINER_ID}" chown -R root:root /root/playwright docker exec "${CONTAINER_ID}" pip install -r local-requirements.txt diff --git a/.github/workflows/trigger_internal_tests.yml b/.github/workflows/trigger_internal_tests.yml deleted file mode 100644 index b301a7b6e..000000000 --- a/.github/workflows/trigger_internal_tests.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: "Internal Tests" - -on: - push: - branches: - - main - - release-* - -jobs: - trigger: - name: "trigger" - runs-on: ubuntu-24.04 - steps: - - uses: actions/create-github-app-token@v1 - id: app-token - with: - app-id: ${{ vars.PLAYWRIGHT_APP_ID }} - private-key: ${{ secrets.PLAYWRIGHT_PRIVATE_KEY }} - repositories: playwright-browsers - - run: | - curl -X POST --fail \ - -H "Accept: application/vnd.github.v3+json" \ - -H "Authorization: token ${GH_TOKEN}" \ - --data "{\"event_type\": \"playwright_tests_python\", \"client_payload\": {\"ref\": \"${GITHUB_SHA}\"}}" \ - https://api.github.com/repos/microsoft/playwright-browsers/dispatches - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5c8c8f1db..57fdca816 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,20 +15,20 @@ repos: - id: check-executables-have-shebangs - id: check-merge-conflict - repo: https://github.com/psf/black - rev: 24.8.0 + rev: 25.1.0 hooks: - id: black - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.11.2 + rev: v1.17.0 hooks: - id: mypy - additional_dependencies: [types-pyOpenSSL==24.1.0.20240722, types-requests==2.32.0.20240914] + additional_dependencies: [types-pyOpenSSL==24.1.0.20240722, types-requests==2.32.4.20250611] - repo: https://github.com/pycqa/flake8 - rev: 7.1.1 + rev: 7.3.0 hooks: - id: flake8 - repo: https://github.com/pycqa/isort - rev: 5.13.2 + rev: 6.0.1 hooks: - id: isort - repo: local @@ -39,7 +39,7 @@ repos: language: node pass_filenames: false types: [python] - additional_dependencies: ["pyright@1.1.384"] + additional_dependencies: ["pyright@1.1.403"] - repo: local hooks: - id: check-license-header diff --git a/README.md b/README.md index b203c6dab..c1797dfe0 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 134.0.6998.35 | ✅ | ✅ | ✅ | -| WebKit 18.4 | ✅ | ✅ | ✅ | -| Firefox 135.0 | ✅ | ✅ | ✅ | +| Chromium 143.0.7499.4 | ✅ | ✅ | ✅ | +| WebKit 26.0 | ✅ | ✅ | ✅ | +| Firefox 144.0.2 | ✅ | ✅ | ✅ | ## Documentation diff --git a/ROLLING.md b/ROLLING.md index f5f500a3f..811d7fcb3 100644 --- a/ROLLING.md +++ b/ROLLING.md @@ -5,10 +5,12 @@ * create virtual environment, if don't have one: `python -m venv env` * activate venv: `source env/bin/activate` * install all deps: - - `python -m pip install --upgrade pip` - - `pip install -r local-requirements.txt` - - `pre-commit install` - - `pip install -e .` +``` +python -m pip install --upgrade pip +pip install -r local-requirements.txt +pre-commit install +pip install -e . +``` * change driver version in `setup.py` * download new driver: `python -m build --wheel` * generate API: `./scripts/update_api.sh` diff --git a/local-requirements.txt b/local-requirements.txt index f0afc5355..8a72b5745 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -1,22 +1,22 @@ autobahn==23.1.2 black==25.1.0 -build==1.2.2.post1 +build==1.3.0 flake8==7.2.0 -mypy==1.15.0 +mypy==1.17.1 objgraph==3.6.2 -Pillow==11.1.0 +Pillow==11.3.0 pixelmatch==0.3.0 pre-commit==3.5.0 -pyOpenSSL==25.0.0 -pytest==8.3.5 -pytest-asyncio==0.26.0 -pytest-cov==6.0.0 -pytest-repeat==0.9.3 -pytest-rerunfailures==15.0 -pytest-timeout==2.3.1 -pytest-xdist==3.6.1 -requests==2.32.3 +pyOpenSSL==25.1.0 +pytest==8.4.1 +pytest-asyncio==1.1.0 +pytest-cov==6.3.0 +pytest-repeat==0.9.4 +pytest-rerunfailures==15.1 +pytest-timeout==2.4.0 +pytest-xdist==3.8.0 +requests==2.32.5 service_identity==24.2.0 -twisted==24.11.0 +twisted==25.5.0 types-pyOpenSSL==24.1.0.20240722 -types-requests==2.32.0.20250306 +types-requests==2.32.4.20250809 diff --git a/playwright/_impl/_accessibility.py b/playwright/_impl/_accessibility.py deleted file mode 100644 index 010b4e8c5..000000000 --- a/playwright/_impl/_accessibility.py +++ /dev/null @@ -1,69 +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. - -from typing import Dict, Optional - -from playwright._impl._connection import Channel -from playwright._impl._element_handle import ElementHandle -from playwright._impl._helper import locals_to_params - - -def _ax_node_from_protocol(axNode: Dict) -> Dict: - result = {**axNode} - if "valueNumber" in axNode: - result["value"] = axNode["valueNumber"] - elif "valueString" in axNode: - result["value"] = axNode["valueString"] - - if "checked" in axNode: - result["checked"] = ( - True - if axNode.get("checked") == "checked" - else ( - False if axNode.get("checked") == "unchecked" else axNode.get("checked") - ) - ) - - if "pressed" in axNode: - result["pressed"] = ( - True - if axNode.get("pressed") == "pressed" - else ( - False if axNode.get("pressed") == "released" else axNode.get("pressed") - ) - ) - - if axNode.get("children"): - result["children"] = list(map(_ax_node_from_protocol, axNode["children"])) - if "valueNumber" in result: - del result["valueNumber"] - if "valueString" in result: - del result["valueString"] - return result - - -class Accessibility: - def __init__(self, channel: Channel) -> None: - self._channel = channel - self._loop = channel._connection._loop - self._dispatcher_fiber = channel._connection._dispatcher_fiber - - async def snapshot( - self, interestingOnly: bool = None, root: ElementHandle = None - ) -> Optional[Dict]: - params = locals_to_params(locals()) - if root: - params["root"] = root._channel - result = await self._channel.send("accessibilitySnapshot", params) - return _ax_node_from_protocol(result) if result else None diff --git a/playwright/_impl/_api_structures.py b/playwright/_impl/_api_structures.py index 3b639486a..c0d0ee442 100644 --- a/playwright/_impl/_api_structures.py +++ b/playwright/_impl/_api_structures.py @@ -32,6 +32,18 @@ class Cookie(TypedDict, total=False): httpOnly: bool secure: bool sameSite: Literal["Lax", "None", "Strict"] + partitionKey: Optional[str] + + +class StorageStateCookie(TypedDict, total=False): + name: str + value: str + domain: str + path: str + expires: float + httpOnly: bool + secure: bool + sameSite: Literal["Lax", "None", "Strict"] # TODO: We are waiting for PEP705 so SetCookieParam can be readonly and matches Cookie. @@ -45,6 +57,7 @@ class SetCookieParam(TypedDict, total=False): httpOnly: Optional[bool] secure: Optional[bool] sameSite: Optional[Literal["Lax", "None", "Strict"]] + partitionKey: Optional[str] class FloatRect(TypedDict): @@ -97,7 +110,7 @@ class ProxySettings(TypedDict, total=False): class StorageState(TypedDict, total=False): - cookies: List[Cookie] + cookies: List[StorageStateCookie] origins: List[OriginState] @@ -205,6 +218,7 @@ class FrameExpectResult(TypedDict): matches: bool received: Any log: List[str] + errorMessage: Optional[str] AriaRole = Literal[ diff --git a/playwright/_impl/_artifact.py b/playwright/_impl/_artifact.py index a5af44573..a08294cbe 100644 --- a/playwright/_impl/_artifact.py +++ b/playwright/_impl/_artifact.py @@ -33,27 +33,55 @@ async def path_after_finished(self) -> pathlib.Path: raise Error( "Path is not available when using browser_type.connect(). Use save_as() to save a local copy." ) - path = await self._channel.send("pathAfterFinished") + path = await self._channel.send( + "pathAfterFinished", + None, + ) return pathlib.Path(path) async def save_as(self, path: Union[str, Path]) -> None: - stream = cast(Stream, from_channel(await self._channel.send("saveAsStream"))) + stream = cast( + Stream, + from_channel( + await self._channel.send( + "saveAsStream", + None, + ) + ), + ) make_dirs_for_file(path) await stream.save_as(path) async def failure(self) -> Optional[str]: - reason = await self._channel.send("failure") + reason = await self._channel.send( + "failure", + None, + ) if reason is None: return None return patch_error_message(reason) async def delete(self) -> None: - await self._channel.send("delete") + await self._channel.send( + "delete", + None, + ) async def read_info_buffer(self) -> bytes: - stream = cast(Stream, from_channel(await self._channel.send("stream"))) + stream = cast( + Stream, + from_channel( + await self._channel.send( + "stream", + None, + ) + ), + ) buffer = await stream.read_all() return buffer async def cancel(self) -> None: # pyright: ignore[reportIncompatibleMethodOverride] - await self._channel.send("cancel") + await self._channel.send( + "cancel", + None, + ) diff --git a/playwright/_impl/_assertions.py b/playwright/_impl/_assertions.py index 8ec657531..aea37d35c 100644 --- a/playwright/_impl/_assertions.py +++ b/playwright/_impl/_assertions.py @@ -20,6 +20,7 @@ AriaRole, ExpectedTextValue, FrameExpectOptions, + FrameExpectResult, ) from playwright._impl._connection import format_call_log from playwright._impl._errors import Error @@ -45,12 +46,20 @@ def __init__( self._is_not = is_not self._custom_message = message + async def _call_expect( + self, expression: str, expect_options: FrameExpectOptions, title: Optional[str] + ) -> FrameExpectResult: + raise NotImplementedError( + "_call_expect must be implemented in a derived class." + ) + async def _expect_impl( self, expression: str, expect_options: FrameExpectOptions, expected: Any, message: str, + title: str = None, ) -> None: __tracebackhide__ = True expect_options["isNot"] = self._is_not @@ -60,7 +69,7 @@ async def _expect_impl( message = message.replace("expected to", "expected not to") if "useInnerText" in expect_options and expect_options["useInnerText"] is None: del expect_options["useInnerText"] - result = await self._actual_locator._expect(expression, expect_options) + result = await self._call_expect(expression, expect_options, title) if result["matches"] == self._is_not: actual = result.get("received") if self._custom_message: @@ -71,8 +80,10 @@ async def _expect_impl( out_message = ( f"{message} '{expected}'" if expected is not None else f"{message}" ) + error_message = result.get("errorMessage") + error_message = f"\n{error_message}" if error_message else "" raise AssertionError( - f"{out_message}\nActual value: {actual} {format_call_log(result.get('log'))}" + f"{out_message}\nActual value: {actual}{error_message} {format_call_log(result.get('log'))}" ) @@ -87,6 +98,14 @@ def __init__( super().__init__(page.locator(":root"), timeout, is_not, message) self._actual_page = page + async def _call_expect( + self, expression: str, expect_options: FrameExpectOptions, title: Optional[str] + ) -> FrameExpectResult: + __tracebackhide__ = True + return await self._actual_page.main_frame._expect( + None, expression, expect_options, title + ) + @property def _not(self) -> "PageAssertions": return PageAssertions( @@ -105,6 +124,7 @@ async def to_have_title( FrameExpectOptions(expectedText=expected_values, timeout=timeout), titleOrRegExp, "Page title expected to be", + 'Expect "to_have_title"', ) async def not_to_have_title( @@ -120,7 +140,7 @@ async def to_have_url( ignoreCase: bool = None, ) -> None: __tracebackhide__ = True - base_url = self._actual_page.context._options.get("baseURL") + base_url = self._actual_page.context._base_url if isinstance(urlOrRegExp, str) and base_url: urlOrRegExp = urljoin(base_url, urlOrRegExp) expected_text = to_expected_text_values([urlOrRegExp], ignoreCase=ignoreCase) @@ -129,6 +149,7 @@ async def to_have_url( FrameExpectOptions(expectedText=expected_text, timeout=timeout), urlOrRegExp, "Page URL expected to be", + 'Expect "to_have_url"', ) async def not_to_have_url( @@ -152,6 +173,12 @@ def __init__( super().__init__(locator, timeout, is_not, message) self._actual_locator = locator + async def _call_expect( + self, expression: str, expect_options: FrameExpectOptions, title: Optional[str] + ) -> FrameExpectResult: + __tracebackhide__ = True + return await self._actual_locator._expect(expression, expect_options, title) + @property def _not(self) -> "LocatorAssertions": return LocatorAssertions( @@ -190,6 +217,7 @@ async def to_contain_text( ), expected, "Locator expected to contain text", + 'Expect "to_contain_text"', ) else: expected_text = to_expected_text_values( @@ -207,6 +235,7 @@ async def to_contain_text( ), expected, "Locator expected to contain text", + 'Expect "to_contain_text"', ) async def not_to_contain_text( @@ -241,6 +270,7 @@ async def to_have_attribute( ), value, "Locator expected to have attribute", + 'Expect "to_have_attribute"', ) async def not_to_have_attribute( @@ -276,6 +306,7 @@ async def to_have_class( FrameExpectOptions(expectedText=expected_text, timeout=timeout), expected, "Locator expected to have class", + 'Expect "to_have_class"', ) else: expected_text = to_expected_text_values([expected]) @@ -284,6 +315,7 @@ async def to_have_class( FrameExpectOptions(expectedText=expected_text, timeout=timeout), expected, "Locator expected to have class", + 'Expect "to_have_class"', ) async def not_to_have_class( @@ -300,6 +332,47 @@ async def not_to_have_class( __tracebackhide__ = True await self._not.to_have_class(expected, timeout) + async def to_contain_class( + self, + expected: Union[ + Sequence[str], + str, + ], + timeout: float = None, + ) -> None: + __tracebackhide__ = True + if isinstance(expected, collections.abc.Sequence) and not isinstance( + expected, str + ): + expected_text = to_expected_text_values(expected) + await self._expect_impl( + "to.contain.class.array", + FrameExpectOptions(expectedText=expected_text, timeout=timeout), + expected, + "Locator expected to contain class names", + 'Expect "to_contain_class"', + ) + else: + expected_text = to_expected_text_values([expected]) + await self._expect_impl( + "to.contain.class", + FrameExpectOptions(expectedText=expected_text, timeout=timeout), + expected, + "Locator expected to contain class", + 'Expect "to_contain_class"', + ) + + async def not_to_contain_class( + self, + expected: Union[ + Sequence[str], + str, + ], + timeout: float = None, + ) -> None: + __tracebackhide__ = True + await self._not.to_contain_class(expected, timeout) + async def to_have_count( self, count: int, @@ -311,6 +384,7 @@ async def to_have_count( FrameExpectOptions(expectedNumber=count, timeout=timeout), count, "Locator expected to have count", + 'Expect "to_have_count"', ) async def not_to_have_count( @@ -336,6 +410,7 @@ async def to_have_css( ), value, "Locator expected to have CSS", + 'Expect "to_have_css"', ) async def not_to_have_css( @@ -359,6 +434,7 @@ async def to_have_id( FrameExpectOptions(expectedText=expected_text, timeout=timeout), id, "Locator expected to have ID", + 'Expect "to_have_id"', ) async def not_to_have_id( @@ -383,6 +459,7 @@ async def to_have_js_property( ), value, "Locator expected to have JS Property", + 'Expect "to_have_property"', ) async def not_to_have_js_property( @@ -406,6 +483,7 @@ async def to_have_value( FrameExpectOptions(expectedText=expected_text, timeout=timeout), value, "Locator expected to have Value", + 'Expect "to_have_value"', ) async def not_to_have_value( @@ -430,6 +508,7 @@ async def to_have_values( FrameExpectOptions(expectedText=expected_text, timeout=timeout), values, "Locator expected to have Values", + 'Expect "to_have_values"', ) async def not_to_have_values( @@ -473,6 +552,7 @@ async def to_have_text( ), expected, "Locator expected to have text", + 'Expect "to_have_text"', ) else: expected_text = to_expected_text_values( @@ -487,6 +567,7 @@ async def to_have_text( ), expected, "Locator expected to have text", + 'Expect "to_have_text"', ) async def not_to_have_text( @@ -519,6 +600,7 @@ async def to_be_attached( FrameExpectOptions(timeout=timeout), None, f"Locator expected to be {attached_string}", + 'Expect "to_be_attached"', ) async def to_be_checked( @@ -543,6 +625,7 @@ async def to_be_checked( FrameExpectOptions(timeout=timeout, expectedValue=expected_value), None, f"Locator expected to be {checked_string}", + 'Expect "to_be_checked"', ) async def not_to_be_attached( @@ -570,6 +653,7 @@ async def to_be_disabled( FrameExpectOptions(timeout=timeout), None, "Locator expected to be disabled", + 'Expect "to_be_disabled"', ) async def not_to_be_disabled( @@ -593,6 +677,7 @@ async def to_be_editable( FrameExpectOptions(timeout=timeout), None, f"Locator expected to be {editable_string}", + 'Expect "to_be_editable"', ) async def not_to_be_editable( @@ -613,6 +698,7 @@ async def to_be_empty( FrameExpectOptions(timeout=timeout), None, "Locator expected to be empty", + 'Expect "to_be_empty"', ) async def not_to_be_empty( @@ -636,6 +722,7 @@ async def to_be_enabled( FrameExpectOptions(timeout=timeout), None, f"Locator expected to be {enabled_string}", + 'Expect "to_be_enabled"', ) async def not_to_be_enabled( @@ -656,6 +743,7 @@ async def to_be_hidden( FrameExpectOptions(timeout=timeout), None, "Locator expected to be hidden", + 'Expect "to_be_hidden"', ) async def not_to_be_hidden( @@ -679,6 +767,7 @@ async def to_be_visible( FrameExpectOptions(timeout=timeout), None, f"Locator expected to be {visible_string}", + 'Expect "to_be_visible"', ) async def not_to_be_visible( @@ -699,6 +788,7 @@ async def to_be_focused( FrameExpectOptions(timeout=timeout), None, "Locator expected to be focused", + 'Expect "to_be_focused"', ) async def not_to_be_focused( @@ -719,6 +809,7 @@ async def to_be_in_viewport( FrameExpectOptions(timeout=timeout, expectedNumber=ratio), None, "Locator expected to be in viewport", + 'Expect "to_be_in_viewport"', ) async def not_to_be_in_viewport( @@ -742,6 +833,7 @@ async def to_have_accessible_description( FrameExpectOptions(expectedText=expected_values, timeout=timeout), None, "Locator expected to have accessible description", + 'Expect "to_have_accessible_description"', ) async def not_to_have_accessible_description( @@ -768,6 +860,7 @@ async def to_have_accessible_name( FrameExpectOptions(expectedText=expected_values, timeout=timeout), None, "Locator expected to have accessible name", + 'Expect "to_have_accessible_name"', ) async def not_to_have_accessible_name( @@ -789,6 +882,7 @@ async def to_have_role(self, role: AriaRole, timeout: float = None) -> None: FrameExpectOptions(expectedText=expected_values, timeout=timeout), None, "Locator expected to have accessible role", + 'Expect "to_have_role"', ) async def to_have_accessible_error_message( @@ -806,6 +900,7 @@ async def to_have_accessible_error_message( FrameExpectOptions(expectedText=expected_values, timeout=timeout), None, "Locator expected to have accessible error message", + 'Expect "to_have_accessible_error_message"', ) async def not_to_have_accessible_error_message( @@ -832,6 +927,7 @@ async def to_match_aria_snapshot( FrameExpectOptions(expectedValue=expected, timeout=timeout), expected, "Locator expected to match Aria snapshot", + 'Expect "to_match_aria_snapshot"', ) async def not_to_match_aria_snapshot( diff --git a/playwright/_impl/_async_base.py b/playwright/_impl/_async_base.py index b06994a65..db7e5d005 100644 --- a/playwright/_impl/_async_base.py +++ b/playwright/_impl/_async_base.py @@ -96,9 +96,9 @@ async def __aenter__(self: Self) -> Self: async def __aexit__( self, - exc_type: Type[BaseException], - exc_val: BaseException, - traceback: TracebackType, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + traceback: Optional[TracebackType], ) -> None: await self.close() diff --git a/playwright/_impl/_browser.py b/playwright/_impl/_browser.py index aa56d8244..5a9a87450 100644 --- a/playwright/_impl/_browser.py +++ b/playwright/_impl/_browser.py @@ -12,10 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json from pathlib import Path from types import SimpleNamespace -from typing import TYPE_CHECKING, Dict, List, Optional, Pattern, Sequence, Union, cast +from typing import ( + TYPE_CHECKING, + Dict, + List, + Optional, + Pattern, + Sequence, + Set, + Union, + cast, +) from playwright._impl._api_structures import ( ClientCertificate, @@ -38,12 +47,9 @@ HarMode, ReducedMotion, ServiceWorkersPolicy, - async_readfile, locals_to_params, make_dirs_for_file, - prepare_record_har_options, ) -from playwright._impl._network import serialize_headers, to_client_certificates_protocol from playwright._impl._page import Page if TYPE_CHECKING: # pragma: no cover @@ -59,28 +65,61 @@ def __init__( self, parent: "BrowserType", type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) - self._browser_type = parent + self._browser_type: Optional["BrowserType"] = None self._is_connected = True self._should_close_connection_on_close = False self._cr_tracing_path: Optional[str] = None - self._contexts: List[BrowserContext] = [] + self._contexts: Set[BrowserContext] = set() + self._traces_dir: Optional[str] = None + self._channel.on( + "context", + lambda params: self._did_create_context( + cast(BrowserContext, from_channel(params["context"])) + ), + ) self._channel.on("close", lambda _: self._on_close()) self._close_reason: Optional[str] = None def __repr__(self) -> str: return f"" + def _connect_to_browser_type( + self, + browser_type: "BrowserType", + traces_dir: Optional[str] = None, + ) -> None: + # Note: when using connect(), `browserType` is different from `this.parent`. + # This is why browser type is not wired up in the constructor, and instead this separate method is called later on. + self._browser_type = browser_type + self._traces_dir = traces_dir + for context in self._contexts: + self._setup_browser_context(context) + + def _did_create_context(self, context: BrowserContext) -> None: + context._browser = self + self._contexts.add(context) + # Note: when connecting to a browser, initial contexts arrive before `_browserType` is set, + # and will be configured later in `ConnectToBrowserType`. + if self._browser_type: + self._setup_browser_context(context) + + def _setup_browser_context(self, context: BrowserContext) -> None: + context._tracing._traces_dir = self._traces_dir + assert self._browser_type is not None + self._browser_type._playwright.selectors._contexts_for_selectors.add(context) + def _on_close(self) -> None: self._is_connected = False self.emit(Browser.Events.Disconnected, self) @property def contexts(self) -> List[BrowserContext]: - return self._contexts.copy() + return list(self._contexts) @property def browser_type(self) -> "BrowserType": + assert self._browser_type is not None return self._browser_type def is_connected(self) -> bool: @@ -126,11 +165,18 @@ async def new_context( clientCertificates: List[ClientCertificate] = None, ) -> BrowserContext: params = locals_to_params(locals()) - await prepare_browser_context_params(params) + assert self._browser_type is not None + await self._browser_type._prepare_browser_context_params(params) - channel = await self._channel.send("newContext", params) + channel = await self._channel.send("newContext", None, params) context = cast(BrowserContext, from_channel(channel)) - self._browser_type._did_create_context(context, params, {}) + await context._initialize_har_from_options( + record_har_content=recordHarContent, + record_har_mode=recordHarMode, + record_har_omit_content=recordHarOmitContent, + record_har_path=recordHarPath, + record_har_url_filter=recordHarUrlFilter, + ) return context async def new_page( @@ -181,7 +227,7 @@ async def inner() -> Page: context._owner_page = page return page - return await self._connection.wrap_api_call(inner) + return await self._connection.wrap_api_call(inner, title="Create page") async def close(self, reason: str = None) -> None: self._close_reason = reason @@ -189,7 +235,7 @@ async def close(self, reason: str = None) -> None: if self._should_close_connection_on_close: await self._connection.stop_async() else: - await self._channel.send("close", {"reason": reason}) + await self._channel.send("close", None, {"reason": reason}) except Exception as e: if not is_target_closed_error(e): raise e @@ -199,7 +245,7 @@ def version(self) -> str: return self._initializer["version"] async def new_browser_cdp_session(self) -> CDPSession: - return from_channel(await self._channel.send("newBrowserCDPSession")) + return from_channel(await self._channel.send("newBrowserCDPSession", None)) async def start_tracing( self, @@ -214,10 +260,12 @@ async def start_tracing( if path: self._cr_tracing_path = str(path) params["path"] = str(path) - await self._channel.send("startTracing", params) + await self._channel.send("startTracing", None, params) async def stop_tracing(self) -> bytes: - artifact = cast(Artifact, from_channel(await self._channel.send("stopTracing"))) + artifact = cast( + Artifact, from_channel(await self._channel.send("stopTracing", None)) + ) buffer = await artifact.read_info_buffer() await artifact.delete() if self._cr_tracing_path: @@ -226,43 +274,3 @@ async def stop_tracing(self) -> bytes: f.write(buffer) self._cr_tracing_path = None return buffer - - -async def prepare_browser_context_params(params: Dict) -> None: - if params.get("noViewport"): - del params["noViewport"] - params["noDefaultViewport"] = True - if "defaultBrowserType" in params: - del params["defaultBrowserType"] - if "extraHTTPHeaders" in params: - params["extraHTTPHeaders"] = serialize_headers(params["extraHTTPHeaders"]) - if "recordHarPath" in params: - params["recordHar"] = prepare_record_har_options(params) - del params["recordHarPath"] - if "recordVideoDir" in params: - params["recordVideo"] = {"dir": Path(params["recordVideoDir"]).absolute()} - if "recordVideoSize" in params: - params["recordVideo"]["size"] = params["recordVideoSize"] - del params["recordVideoSize"] - del params["recordVideoDir"] - if "storageState" in params: - storageState = params["storageState"] - if not isinstance(storageState, dict): - params["storageState"] = json.loads( - (await async_readfile(storageState)).decode() - ) - if params.get("colorScheme", None) == "null": - params["colorScheme"] = "no-override" - if params.get("reducedMotion", None) == "null": - params["reducedMotion"] = "no-override" - if params.get("forcedColors", None) == "null": - params["forcedColors"] = "no-override" - if params.get("contrast", None) == "null": - params["contrast"] = "no-override" - if "acceptDownloads" in params: - params["acceptDownloads"] = "accept" if params["acceptDownloads"] else "deny" - - if "clientCertificates" in params: - params["clientCertificates"] = await to_client_certificates_protocol( - params["clientCertificates"] - ) diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index 22da4375d..f56564a27 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -66,7 +66,6 @@ async_writefile, locals_to_params, parse_error, - prepare_record_har_options, to_impl, ) from playwright._impl._network import ( @@ -89,6 +88,7 @@ class BrowserContext(ChannelOwner): Events = SimpleNamespace( + # Deprecated in v1.56, never emitted anymore. BackgroundPage="backgroundpage", Close="close", Console="console", @@ -106,20 +106,21 @@ def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) + # Browser is null for browser contexts created outside of normal browser, e.g. android or electron. # circular import workaround: self._browser: Optional["Browser"] = None if parent.__class__.__name__ == "Browser": self._browser = cast("Browser", parent) - self._browser._contexts.append(self) self._pages: List[Page] = [] self._routes: List[RouteHandler] = [] self._web_socket_routes: List[WebSocketRouteHandler] = [] self._bindings: Dict[str, Any] = {} self._timeout_settings = TimeoutSettings(None) self._owner_page: Optional[Page] = None - self._options: Dict[str, Any] = {} - self._background_pages: Set[Page] = set() + self._options: Dict[str, Any] = initializer["options"] self._service_workers: Set[Worker] = set() + self._base_url: Optional[str] = self._options.get("baseURL") + self._videos_dir: Optional[str] = self._options.get("recordVideo") self._tracing = cast(Tracing, from_channel(initializer["tracing"])) self._har_recorders: Dict[str, HarRecordingMetadata] = {} self._request: APIRequestContext = from_channel(initializer["requestContext"]) @@ -148,10 +149,6 @@ def __init__( ) ), ) - self._channel.on( - "backgroundPage", - lambda params: self._on_background_page(from_channel(params["page"])), - ) self._channel.on( "serviceWorker", @@ -220,7 +217,7 @@ def __init__( BrowserContext.Events.RequestFailed: "requestFailed", } ) - self._close_was_called = False + self._closing_or_closed = False def __repr__(self) -> str: return f"" @@ -237,7 +234,7 @@ async def _on_route(self, route: Route) -> None: route_handlers = self._routes.copy() for route_handler in route_handlers: # If the page or the context was closed we stall all requests right away. - if (page and page._close_was_called) or self._close_was_called: + if (page and page._close_was_called) or self._closing_or_closed: return if not route_handler.matches(route.request.url): continue @@ -288,19 +285,12 @@ def set_default_navigation_timeout(self, timeout: float) -> None: def _set_default_navigation_timeout_impl(self, timeout: Optional[float]) -> None: self._timeout_settings.set_default_navigation_timeout(timeout) - self._channel.send_no_reply( - "setDefaultNavigationTimeoutNoReply", - {} if timeout is None else {"timeout": timeout}, - ) def set_default_timeout(self, timeout: float) -> None: return self._set_default_timeout_impl(timeout) def _set_default_timeout_impl(self, timeout: Optional[float]) -> None: self._timeout_settings.set_default_timeout(timeout) - self._channel.send_no_reply( - "setDefaultTimeoutNoReply", {} if timeout is None else {"timeout": timeout} - ) @property def pages(self) -> List[Page]: @@ -310,29 +300,45 @@ def pages(self) -> List[Page]: def browser(self) -> Optional["Browser"]: return self._browser - def _set_options(self, context_options: Dict, browser_options: Dict) -> None: - self._options = context_options - if self._options.get("recordHar"): - self._har_recorders[""] = { - "path": self._options["recordHar"]["path"], - "content": self._options["recordHar"].get("content"), - } - self._tracing._traces_dir = browser_options.get("tracesDir") + async def _initialize_har_from_options( + self, + record_har_path: Optional[Union[Path, str]], + record_har_content: Optional[HarContentPolicy], + record_har_omit_content: Optional[bool], + record_har_url_filter: Optional[Union[Pattern[str], str]], + record_har_mode: Optional[HarMode], + ) -> None: + if not record_har_path: + return + record_har_path = str(record_har_path) + default_policy: HarContentPolicy = ( + "attach" if record_har_path.endswith(".zip") else "embed" + ) + content_policy: HarContentPolicy = record_har_content or ( + "omit" if record_har_omit_content is True else default_policy + ) + await self._record_into_har( + har=record_har_path, + page=None, + url=record_har_url_filter, + update_content=content_policy, + update_mode=(record_har_mode or "full"), + ) async def new_page(self) -> Page: if self._owner_page: raise Error("Please use browser.new_context()") - return from_channel(await self._channel.send("newPage")) + return from_channel(await self._channel.send("newPage", None)) async def cookies(self, urls: Union[str, Sequence[str]] = None) -> List[Cookie]: if urls is None: urls = [] if isinstance(urls, str): urls = [urls] - return await self._channel.send("cookies", dict(urls=urls)) + return await self._channel.send("cookies", None, dict(urls=urls)) async def add_cookies(self, cookies: Sequence[SetCookieParam]) -> None: - await self._channel.send("addCookies", dict(cookies=cookies)) + await self._channel.send("addCookies", None, dict(cookies=cookies)) async def clear_cookies( self, @@ -342,6 +348,7 @@ async def clear_cookies( ) -> None: await self._channel.send( "clearCookies", + None, { "name": name if isinstance(name, str) else None, "nameRegexSource": name.pattern if isinstance(name, Pattern) else None, @@ -366,21 +373,21 @@ async def clear_cookies( async def grant_permissions( self, permissions: Sequence[str], origin: str = None ) -> None: - await self._channel.send("grantPermissions", locals_to_params(locals())) + await self._channel.send("grantPermissions", None, locals_to_params(locals())) async def clear_permissions(self) -> None: - await self._channel.send("clearPermissions") + await self._channel.send("clearPermissions", None) async def set_geolocation(self, geolocation: Geolocation = None) -> None: - await self._channel.send("setGeolocation", locals_to_params(locals())) + await self._channel.send("setGeolocation", None, locals_to_params(locals())) async def set_extra_http_headers(self, headers: Dict[str, str]) -> None: await self._channel.send( - "setExtraHTTPHeaders", dict(headers=serialize_headers(headers)) + "setExtraHTTPHeaders", None, dict(headers=serialize_headers(headers)) ) async def set_offline(self, offline: bool) -> None: - await self._channel.send("setOffline", dict(offline=offline)) + await self._channel.send("setOffline", None, dict(offline=offline)) async def add_init_script( self, script: str = None, path: Union[str, Path] = None @@ -389,7 +396,7 @@ async def add_init_script( script = (await async_readfile(path)).decode() if not isinstance(script, str): raise Error("Either path or script parameter must be specified") - await self._channel.send("addInitScript", dict(source=script)) + await self._channel.send("addInitScript", None, dict(source=script)) async def expose_binding( self, name: str, callback: Callable, handle: bool = None @@ -403,7 +410,7 @@ async def expose_binding( raise Error(f'Function "{name}" has been already registered') self._bindings[name] = callback await self._channel.send( - "exposeBinding", dict(name=name, needsHandle=handle or False) + "exposeBinding", None, dict(name=name, needsHandle=handle or False) ) async def expose_function(self, name: str, callback: Callable) -> None: @@ -415,7 +422,7 @@ async def route( self._routes.insert( 0, RouteHandler( - self._options.get("baseURL"), + self._base_url, url, handler, True if self._dispatcher_fiber else False, @@ -443,17 +450,16 @@ async def _unroute_internal( behavior: Literal["default", "ignoreErrors", "wait"] = None, ) -> None: self._routes = remaining + if behavior is not None and behavior != "default": + await asyncio.gather(*map(lambda router: router.stop(behavior), removed)) # type: ignore await self._update_interception_patterns() - if behavior is None or behavior == "default": - return - await asyncio.gather(*map(lambda router: router.stop(behavior), removed)) # type: ignore async def route_web_socket( self, url: URLMatch, handler: WebSocketRouteHandlerCallback ) -> None: self._web_socket_routes.insert( 0, - WebSocketRouteHandler(self._options.get("baseURL"), url, handler), + WebSocketRouteHandler(self._base_url, url, handler), ) await self._update_web_socket_interception_patterns() @@ -476,22 +482,25 @@ async def _record_into_har( update_content: HarContentPolicy = None, update_mode: HarMode = None, ) -> None: + update_content = update_content or "attach" params: Dict[str, Any] = { - "options": prepare_record_har_options( - { - "recordHarPath": har, - "recordHarContent": update_content or "attach", - "recordHarMode": update_mode or "minimal", - "recordHarUrlFilter": url, - } - ) + "options": { + "zip": str(har).endswith(".zip"), + "content": update_content, + "urlGlob": url if isinstance(url, str) else None, + "urlRegexSource": url.pattern if isinstance(url, Pattern) else None, + "urlRegexFlags": ( + escape_regex_flags(url) if isinstance(url, Pattern) else None + ), + "mode": update_mode or "minimal", + } } if page: params["page"] = page._channel - har_id = await self._channel.send("harStart", params) + har_id = await self._channel.send("harStart", None, params) self._har_recorders[har_id] = { "path": str(har), - "content": update_content or "attach", + "content": update_content, } async def route_from_har( @@ -524,7 +533,7 @@ async def route_from_har( async def _update_interception_patterns(self) -> None: patterns = RouteHandler.prepare_interception_patterns(self._routes) await self._channel.send( - "setNetworkInterceptionPatterns", {"patterns": patterns} + "setNetworkInterceptionPatterns", None, {"patterns": patterns} ) async def _update_web_socket_interception_patterns(self) -> None: @@ -532,7 +541,7 @@ async def _update_web_socket_interception_patterns(self) -> None: self._web_socket_routes ) await self._channel.send( - "setWebSocketInterceptionPatterns", {"patterns": patterns} + "setWebSocketInterceptionPatterns", None, {"patterns": patterns} ) def expect_event( @@ -555,29 +564,37 @@ def expect_event( return EventContextManagerImpl(waiter.result()) def _on_close(self) -> None: + self._closing_or_closed = True if self._browser: - self._browser._contexts.remove(self) + if self in self._browser._contexts: + self._browser._contexts.remove(self) + assert self._browser._browser_type is not None + if ( + self + in self._browser._browser_type._playwright.selectors._contexts_for_selectors + ): + self._browser._browser_type._playwright.selectors._contexts_for_selectors.remove( + self + ) self._dispose_har_routers() self._tracing._reset_stack_counter() self.emit(BrowserContext.Events.Close, self) async def close(self, reason: str = None) -> None: - if self._close_was_called: + if self._closing_or_closed: return self._close_reason = reason - self._close_was_called = True + self._closing_or_closed = True - await self._channel._connection.wrap_api_call( - lambda: self.request.dispose(reason=reason), True - ) + await self.request.dispose(reason=reason) async def _inner_close() -> None: for har_id, params in self._har_recorders.items(): har = cast( Artifact, from_channel( - await self._channel.send("harExport", {"harId": har_id}) + await self._channel.send("harExport", None, {"harId": har_id}) ), ) # Server side will compress artifact if content is attach or if file is .zip. @@ -596,14 +613,14 @@ async def _inner_close() -> None: await har.delete() await self._channel._connection.wrap_api_call(_inner_close, True) - await self._channel.send("close", {"reason": reason}) + await self._channel.send("close", None, {"reason": reason}) await self._closed_future async def storage_state( self, path: Union[str, Path] = None, indexedDB: bool = None ) -> StorageState: result = await self._channel.send_return_as_dict( - "storageState", {"indexedDB": indexedDB} + "storageState", None, {"indexedDB": indexedDB} ) if path: await async_writefile(path, json.dumps(result)) @@ -637,10 +654,6 @@ def expect_page( ) -> EventContextManagerImpl[Page]: return self.expect_event(BrowserContext.Events.Page, predicate, timeout) - def _on_background_page(self, page: Page) -> None: - self._background_pages.add(page) - self.emit(BrowserContext.Events.BackgroundPage, page) - def _on_service_worker(self, worker: Worker) -> None: worker._context = self self._service_workers.add(worker) @@ -675,10 +688,13 @@ def _on_request_finished( def _on_console_message(self, event: Dict) -> None: message = ConsoleMessage(event, self._loop, self._dispatcher_fiber) - self.emit(BrowserContext.Events.Console, message) + worker = message.worker + if worker: + worker.emit(Worker.Events.Console, message) page = message.page if page: page.emit(Page.Events.Console, message) + self.emit(BrowserContext.Events.Console, message) def _on_dialog(self, dialog: Dialog) -> None: has_listeners = self.emit(BrowserContext.Events.Dialog, dialog) @@ -715,7 +731,7 @@ def _on_response(self, response: Response, page: Optional[Page]) -> None: @property def background_pages(self) -> List[Page]: - return list(self._background_pages) + return [] @property def service_workers(self) -> List[Worker]: @@ -730,7 +746,7 @@ async def new_cdp_session(self, page: Union[Page, Frame]) -> CDPSession: params["frame"] = page._channel else: raise Error("page: expected Page or Frame") - return from_channel(await self._channel.send("newCDPSession", params)) + return from_channel(await self._channel.send("newCDPSession", None, params)) @property def tracing(self) -> Tracing: diff --git a/playwright/_impl/_browser_type.py b/playwright/_impl/_browser_type.py index b34d224d6..93173160c 100644 --- a/playwright/_impl/_browser_type.py +++ b/playwright/_impl/_browser_type.py @@ -13,6 +13,7 @@ # limitations under the License. import asyncio +import json import pathlib import sys from pathlib import Path @@ -25,16 +26,12 @@ ProxySettings, ViewportSize, ) -from playwright._impl._browser import Browser, prepare_browser_context_params +from playwright._impl._browser import Browser from playwright._impl._browser_context import BrowserContext -from playwright._impl._connection import ( - ChannelOwner, - Connection, - from_channel, - from_nullable_channel, -) +from playwright._impl._connection import ChannelOwner, Connection, from_channel from playwright._impl._errors import Error from playwright._impl._helper import ( + PLAYWRIGHT_MAX_DEADLINE, ColorScheme, Contrast, Env, @@ -43,10 +40,12 @@ HarMode, ReducedMotion, ServiceWorkersPolicy, + TimeoutSettings, + async_readfile, locals_to_params, ) from playwright._impl._json_pipe import JsonPipeTransport -from playwright._impl._network import serialize_headers +from playwright._impl._network import serialize_headers, to_client_certificates_protocol from playwright._impl._waiter import throw_on_timeout if TYPE_CHECKING: @@ -94,9 +93,16 @@ async def launch( params = locals_to_params(locals()) normalize_launch_params(params) browser = cast( - Browser, from_channel(await self._channel.send("launch", params)) + Browser, + from_channel( + await self._channel.send( + "launch", TimeoutSettings.launch_timeout, params + ) + ), + ) + browser._connect_to_browser_type( + self, str(tracesDir) if tracesDir is not None else None ) - self._did_launch_browser(browser) return browser async def launch_persistent_context( @@ -155,13 +161,26 @@ async def launch_persistent_context( ) -> BrowserContext: userDataDir = self._user_data_dir(userDataDir) params = locals_to_params(locals()) - await prepare_browser_context_params(params) + await self._prepare_browser_context_params(params) normalize_launch_params(params) - context = cast( - BrowserContext, - from_channel(await self._channel.send("launchPersistentContext", params)), + result = await self._channel.send_return_as_dict( + "launchPersistentContext", TimeoutSettings.launch_timeout, params + ) + browser = cast( + Browser, + from_channel(result["browser"]), + ) + browser._connect_to_browser_type( + self, str(tracesDir) if tracesDir is not None else None + ) + context = cast(BrowserContext, from_channel(result["context"])) + await context._initialize_har_from_options( + record_har_content=recordHarContent, + record_har_mode=recordHarMode, + record_har_omit_content=recordHarOmitContent, + record_har_path=recordHarPath, + record_har_url_filter=recordHarUrlFilter, ) - self._did_create_context(context, params, params) return context def _user_data_dir(self, userDataDir: Optional[Union[str, Path]]) -> str: @@ -171,7 +190,7 @@ def _user_data_dir(self, userDataDir: Optional[Union[str, Path]]) -> str: # Can be dropped once we drop Python 3.9 support (10/2025): # https://github.com/python/cpython/issues/82852 if sys.platform == "win32" and sys.version_info[:2] < (3, 10): - return pathlib.Path.cwd() / userDataDir + return str(pathlib.Path.cwd() / userDataDir) return str(Path(userDataDir).resolve()) return str(Path(userDataDir)) @@ -185,16 +204,12 @@ async def connect_over_cdp( params = locals_to_params(locals()) if params.get("headers"): params["headers"] = serialize_headers(params["headers"]) - response = await self._channel.send_return_as_dict("connectOverCDP", params) + response = await self._channel.send_return_as_dict( + "connectOverCDP", TimeoutSettings.launch_timeout, params + ) browser = cast(Browser, from_channel(response["browser"])) - self._did_launch_browser(browser) + browser._connect_to_browser_type(self, None) - default_context = cast( - Optional[BrowserContext], - from_nullable_channel(response.get("defaultContext")), - ) - if default_context: - self._did_create_context(default_context, {}, {}) return browser async def connect( @@ -205,8 +220,6 @@ async def connect( headers: Dict[str, str] = None, exposeNetwork: str = None, ) -> Browser: - if timeout is None: - timeout = 30000 if slowMo is None: slowMo = 0 @@ -215,11 +228,12 @@ async def connect( pipe_channel = ( await local_utils._channel.send_return_as_dict( "connect", + None, { "wsEndpoint": wsEndpoint, "headers": headers, "slowMo": slowMo, - "timeout": timeout, + "timeout": timeout if timeout is not None else 0, "exposeNetwork": exposeNetwork, }, ) @@ -259,7 +273,10 @@ def handle_transport_close(reason: Optional[str]) -> None: connection._loop.create_task(connection.run()) playwright_future = connection.playwright_future - timeout_future = throw_on_timeout(timeout, Error("Connection timed out")) + timeout_future = throw_on_timeout( + timeout if timeout is not None else PLAYWRIGHT_MAX_DEADLINE, + Error("Connection timed out"), + ) done, pending = await asyncio.wait( {transport.on_error_future, playwright_future, timeout_future}, return_when=asyncio.FIRST_COMPLETED, @@ -274,18 +291,59 @@ def handle_transport_close(reason: Optional[str]) -> None: pre_launched_browser = playwright._initializer.get("preLaunchedBrowser") assert pre_launched_browser browser = cast(Browser, from_channel(pre_launched_browser)) - self._did_launch_browser(browser) browser._should_close_connection_on_close = True + browser._connect_to_browser_type(self, None) return browser - def _did_create_context( - self, context: BrowserContext, context_options: Dict, browser_options: Dict - ) -> None: - context._set_options(context_options, browser_options) + async def _prepare_browser_context_params(self, params: Dict) -> None: + if params.get("noViewport"): + del params["noViewport"] + params["noDefaultViewport"] = True + if "defaultBrowserType" in params: + del params["defaultBrowserType"] + if "extraHTTPHeaders" in params: + params["extraHTTPHeaders"] = serialize_headers(params["extraHTTPHeaders"]) + if "recordVideoDir" in params: + params["recordVideo"] = {"dir": Path(params["recordVideoDir"]).absolute()} + if "recordVideoSize" in params: + params["recordVideo"]["size"] = params["recordVideoSize"] + del params["recordVideoSize"] + del params["recordVideoDir"] + if "storageState" in params: + storageState = params["storageState"] + if not isinstance(storageState, dict): + params["storageState"] = json.loads( + (await async_readfile(storageState)).decode() + ) + if params.get("colorScheme", None) == "null": + params["colorScheme"] = "no-override" + if params.get("reducedMotion", None) == "null": + params["reducedMotion"] = "no-override" + if params.get("forcedColors", None) == "null": + params["forcedColors"] = "no-override" + if params.get("contrast", None) == "null": + params["contrast"] = "no-override" + if "acceptDownloads" in params: + params["acceptDownloads"] = ( + "accept" if params["acceptDownloads"] else "deny" + ) + + if "clientCertificates" in params: + params["clientCertificates"] = await to_client_certificates_protocol( + params["clientCertificates"] + ) + params["selectorEngines"] = self._playwright.selectors._selector_engines + params["testIdAttributeName"] = ( + self._playwright.selectors._test_id_attribute_name + ) - def _did_launch_browser(self, browser: Browser) -> None: - browser._browser_type = self + # Remove HAR options + params.pop("recordHarPath", None) + params.pop("recordHarOmitContent", None) + params.pop("recordHarUrlFilter", None) + params.pop("recordHarMode", None) + params.pop("recordHarContent", None) def normalize_launch_params(params: Dict) -> None: diff --git a/playwright/_impl/_cdp_session.py b/playwright/_impl/_cdp_session.py index b6e383ff2..95e65c57a 100644 --- a/playwright/_impl/_cdp_session.py +++ b/playwright/_impl/_cdp_session.py @@ -29,7 +29,10 @@ def _on_event(self, params: Any) -> None: self.emit(params["method"], params.get("params")) async def send(self, method: str, params: Dict = None) -> Dict: - return await self._channel.send("send", locals_to_params(locals())) + return await self._channel.send("send", None, locals_to_params(locals())) async def detach(self) -> None: - await self._channel.send("detach") + await self._channel.send( + "detach", + None, + ) diff --git a/playwright/_impl/_clock.py b/playwright/_impl/_clock.py index d8bb58718..f6eb7c42d 100644 --- a/playwright/_impl/_clock.py +++ b/playwright/_impl/_clock.py @@ -27,7 +27,9 @@ def __init__(self, browser_context: "BrowserContext") -> None: async def install(self, time: Union[float, str, datetime.datetime] = None) -> None: await self._browser_context._channel.send( - "clockInstall", parse_time(time) if time is not None else {} + "clockInstall", + None, + parse_time(time) if time is not None else {}, ) async def fast_forward( @@ -35,43 +37,59 @@ async def fast_forward( ticks: Union[int, str], ) -> None: await self._browser_context._channel.send( - "clockFastForward", parse_ticks(ticks) + "clockFastForward", + None, + parse_ticks(ticks), ) async def pause_at( self, time: Union[float, str, datetime.datetime], ) -> None: - await self._browser_context._channel.send("clockPauseAt", parse_time(time)) + await self._browser_context._channel.send( + "clockPauseAt", + None, + parse_time(time), + ) async def resume( self, ) -> None: - await self._browser_context._channel.send("clockResume") + await self._browser_context._channel.send("clockResume", None) async def run_for( self, ticks: Union[int, str], ) -> None: - await self._browser_context._channel.send("clockRunFor", parse_ticks(ticks)) + await self._browser_context._channel.send( + "clockRunFor", + None, + parse_ticks(ticks), + ) async def set_fixed_time( self, time: Union[float, str, datetime.datetime], ) -> None: - await self._browser_context._channel.send("clockSetFixedTime", parse_time(time)) + await self._browser_context._channel.send( + "clockSetFixedTime", + None, + parse_time(time), + ) async def set_system_time( self, time: Union[float, str, datetime.datetime], ) -> None: await self._browser_context._channel.send( - "clockSetSystemTime", parse_time(time) + "clockSetSystemTime", + None, + parse_time(time), ) def parse_time( - time: Union[float, str, datetime.datetime] + time: Union[float, str, datetime.datetime], ) -> Dict[str, Union[int, str]]: if isinstance(time, (float, int)): return {"timeNumber": int(time * 1_000)} diff --git a/playwright/_impl/_connection.py b/playwright/_impl/_connection.py index 027daf69d..bbc42b6e1 100644 --- a/playwright/_impl/_connection.py +++ b/playwright/_impl/_connection.py @@ -47,6 +47,8 @@ from playwright._impl._local_utils import LocalUtils from playwright._impl._playwright import Playwright +TimeoutCalculator = Optional[Callable[[Optional[float]], float]] + class Channel(AsyncIOEventEmitter): def __init__(self, connection: "Connection", object: "ChannelOwner") -> None: @@ -55,39 +57,68 @@ def __init__(self, connection: "Connection", object: "ChannelOwner") -> None: self._guid = object._guid self._object = object self.on("error", lambda exc: self._connection._on_event_listener_error(exc)) - self._is_internal_type = False - async def send(self, method: str, params: Dict = None) -> Any: + async def send( + self, + method: str, + timeout_calculator: TimeoutCalculator, + params: Dict = None, + is_internal: bool = False, + title: str = None, + ) -> Any: return await self._connection.wrap_api_call( - lambda: self._inner_send(method, params, False), - self._is_internal_type, + lambda: self._inner_send(method, timeout_calculator, params, False), + is_internal, + title, ) - async def send_return_as_dict(self, method: str, params: Dict = None) -> Any: + async def send_return_as_dict( + self, + method: str, + timeout_calculator: TimeoutCalculator, + params: Dict = None, + is_internal: bool = False, + title: str = None, + ) -> Any: return await self._connection.wrap_api_call( - lambda: self._inner_send(method, params, True), - self._is_internal_type, + lambda: self._inner_send(method, timeout_calculator, params, True), + is_internal, + title, ) - def send_no_reply(self, method: str, params: Dict = None) -> None: + def send_no_reply( + self, + method: str, + timeout_calculator: TimeoutCalculator, + params: Dict = None, + is_internal: bool = False, + title: str = None, + ) -> None: # No reply messages are used to e.g. waitForEventInfo(after). self._connection.wrap_api_call_sync( lambda: self._connection._send_message_to_server( - self._object, method, {} if params is None else params, True - ) + self._object, + method, + _augment_params(params, timeout_calculator), + True, + ), + is_internal, + title, ) async def _inner_send( - self, method: str, params: Optional[Dict], return_as_dict: bool + self, + method: str, + timeout_calculator: TimeoutCalculator, + params: Optional[Dict], + return_as_dict: bool, ) -> Any: - if params is None: - params = {} if self._connection._error: error = self._connection._error self._connection._error = None raise error callback = self._connection._send_message_to_server( - self._object, method, _filter_none(params) + self._object, method, _augment_params(params, timeout_calculator) ) done, _ = await asyncio.wait( { @@ -112,9 +143,6 @@ async def _inner_send( key = next(iter(result)) return result[key] - def mark_as_internal_type(self) -> None: - self._is_internal_type = True - class ChannelOwner(AsyncIOEventEmitter): def __init__( @@ -171,7 +199,9 @@ def _update_subscription(self, event: str, enabled: bool) -> None: if protocol_event: self._connection.wrap_api_call_sync( lambda: self._channel.send_no_reply( - "updateSubscription", {"event": protocol_event, "enabled": enabled} + "updateSubscription", + None, + {"event": protocol_event, "enabled": enabled}, ), True, ) @@ -188,10 +218,12 @@ def remove_listener(self, event: str, f: Any) -> None: class ProtocolCallback: - def __init__(self, loop: asyncio.AbstractEventLoop) -> None: + def __init__(self, loop: asyncio.AbstractEventLoop, no_reply: bool = False) -> None: self.stack_trace: traceback.StackSummary - self.no_reply: bool + self.no_reply = no_reply self.future = loop.create_future() + if no_reply: + self.future.set_result(None) # The outer task can get cancelled by the user, this forwards the cancellation to the inner task. current_task = asyncio.current_task() @@ -218,6 +250,7 @@ async def initialize(self) -> "Playwright": return from_channel( await self._channel.send( "initialize", + None, { "sdkLanguage": "python", }, @@ -329,14 +362,13 @@ def _send_message_to_server( ) self._last_id += 1 id = self._last_id - callback = ProtocolCallback(self._loop) + callback = ProtocolCallback(self._loop, no_reply=no_reply) task = asyncio.current_task(self._loop) callback.stack_trace = cast( traceback.StackSummary, - getattr(task, "__pw_stack_trace__", traceback.extract_stack()), + getattr(task, "__pw_stack_trace__", traceback.extract_stack(limit=10)), ) callback.no_reply = no_reply - self._callbacks[id] = callback stack_trace_information = cast(ParsedStackTrace, self._api_zone.get()) frames = stack_trace_information.get("frames", []) location = ( @@ -355,6 +387,9 @@ def _send_message_to_server( } if location: metadata["location"] = location # type: ignore + title = stack_trace_information["title"] + if title: + metadata["title"] = title message = { "id": id, "guid": object._guid, @@ -362,16 +397,11 @@ def _send_message_to_server( "params": self._replace_channels_with_guids(params), "metadata": metadata, } - if ( - self._tracing_count > 0 - and frames - and frames - and object._guid != "localUtils" - ): + if self._tracing_count > 0 and frames and object._guid != "localUtils": self.local_utils.add_stack_to_tracing_no_reply(id, frames) - self._transport.send(message) self._callbacks[id] = callback + self._transport.send(message) return callback @@ -392,9 +422,7 @@ def dispatch(self, msg: ParsedMessagePayload) -> None: parsed_error = parse_error( error["error"], format_call_log(msg.get("log")) # type: ignore ) - parsed_error._stack = "".join( - traceback.format_list(callback.stack_trace)[-10:] - ) + parsed_error._stack = "".join(callback.stack_trace.format()) callback.future.set_exception(parsed_error) else: result = self._replace_guids_with_channels(msg.get("result")) @@ -514,13 +542,16 @@ def _replace_guids_with_channels(self, payload: Any) -> Any: return payload async def wrap_api_call( - self, cb: Callable[[], Any], is_internal: bool = False + self, cb: Callable[[], Any], is_internal: bool = False, title: str = None ) -> Any: if self._api_zone.get(): return await cb() task = asyncio.current_task(self._loop) - st: List[inspect.FrameInfo] = getattr(task, "__pw_stack__", inspect.stack()) - parsed_st = _extract_stack_trace_information_from_stack(st, is_internal) + st: List[inspect.FrameInfo] = getattr( + task, "__pw_stack__", None + ) or inspect.stack(0) + + parsed_st = _extract_stack_trace_information_from_stack(st, is_internal, title) self._api_zone.set(parsed_st) try: return await cb() @@ -530,13 +561,15 @@ async def wrap_api_call( self._api_zone.set(None) def wrap_api_call_sync( - self, cb: Callable[[], Any], is_internal: bool = False + self, cb: Callable[[], Any], is_internal: bool = False, title: str = None ) -> Any: if self._api_zone.get(): return cb() task = asyncio.current_task(self._loop) - st: List[inspect.FrameInfo] = getattr(task, "__pw_stack__", inspect.stack()) - parsed_st = _extract_stack_trace_information_from_stack(st, is_internal) + st: List[inspect.FrameInfo] = getattr( + task, "__pw_stack__", None + ) or inspect.stack(0) + parsed_st = _extract_stack_trace_information_from_stack(st, is_internal, title) self._api_zone.set(parsed_st) try: return cb() @@ -564,10 +597,11 @@ class StackFrame(TypedDict): class ParsedStackTrace(TypedDict): frames: List[StackFrame] apiName: Optional[str] + title: Optional[str] def _extract_stack_trace_information_from_stack( - st: List[inspect.FrameInfo], is_internal: bool + st: List[inspect.FrameInfo], is_internal: bool, title: str = None ) -> ParsedStackTrace: playwright_module_path = str(Path(playwright.__file__).parents[0]) last_internal_api_name = "" @@ -607,11 +641,28 @@ def _extract_stack_trace_information_from_stack( return { "frames": parsed_frames, "apiName": "" if is_internal else api_name, + "title": title, } +def _augment_params( + params: Optional[Dict], + timeout_calculator: Optional[Callable[[Optional[float]], float]], +) -> Dict: + if params is None: + params = {} + if timeout_calculator: + params["timeout"] = timeout_calculator(params.get("timeout")) + return _filter_none(params) + + def _filter_none(d: Mapping) -> Dict: - return {k: v for k, v in d.items() if v is not None} + result = {} + for k, v in d.items(): + if v is None: + continue + result[k] = _filter_none(v) if isinstance(v, dict) else v + return result def format_call_log(log: Optional[List[str]]) -> str: diff --git a/playwright/_impl/_console_message.py b/playwright/_impl/_console_message.py index ba8fc0a38..7866df2ae 100644 --- a/playwright/_impl/_console_message.py +++ b/playwright/_impl/_console_message.py @@ -13,7 +13,7 @@ # limitations under the License. from asyncio import AbstractEventLoop -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union from playwright._impl._api_structures import SourceLocation from playwright._impl._connection import from_channel, from_nullable_channel @@ -21,6 +21,7 @@ if TYPE_CHECKING: # pragma: no cover from playwright._impl._page import Page + from playwright._impl._worker import Worker class ConsoleMessage: @@ -31,6 +32,7 @@ def __init__( self._loop = loop self._dispatcher_fiber = dispatcher_fiber self._page: Optional["Page"] = from_nullable_channel(event.get("page")) + self._worker: Optional["Worker"] = from_nullable_channel(event.get("worker")) def __repr__(self) -> str: return f"" @@ -39,7 +41,26 @@ def __str__(self) -> str: return self.text @property - def type(self) -> str: + def type(self) -> Union[ + Literal["assert"], + Literal["clear"], + Literal["count"], + Literal["debug"], + Literal["dir"], + Literal["dirxml"], + Literal["endGroup"], + Literal["error"], + Literal["info"], + Literal["log"], + Literal["profile"], + Literal["profileEnd"], + Literal["startGroup"], + Literal["startGroupCollapsed"], + Literal["table"], + Literal["timeEnd"], + Literal["trace"], + Literal["warning"], + ]: return self._event["type"] @property @@ -57,3 +78,7 @@ def location(self) -> SourceLocation: @property def page(self) -> Optional["Page"]: return self._page + + @property + def worker(self) -> Optional["Worker"]: + return self._worker diff --git a/playwright/_impl/_dialog.py b/playwright/_impl/_dialog.py index a0c6ca77f..226e703b9 100644 --- a/playwright/_impl/_dialog.py +++ b/playwright/_impl/_dialog.py @@ -48,7 +48,10 @@ def page(self) -> Optional["Page"]: return self._page async def accept(self, promptText: str = None) -> None: - await self._channel.send("accept", locals_to_params(locals())) + await self._channel.send("accept", None, locals_to_params(locals())) async def dismiss(self) -> None: - await self._channel.send("dismiss") + await self._channel.send( + "dismiss", + None, + ) diff --git a/playwright/_impl/_element_handle.py b/playwright/_impl/_element_handle.py index cb3d672d4..3854669d0 100644 --- a/playwright/_impl/_element_handle.py +++ b/playwright/_impl/_element_handle.py @@ -13,6 +13,7 @@ # limitations under the License. import base64 +import mimetypes from pathlib import Path from typing import ( TYPE_CHECKING, @@ -55,56 +56,63 @@ def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) + self._frame = cast("Frame", parent) async def _createSelectorForTest(self, name: str) -> Optional[str]: - return await self._channel.send("createSelectorForTest", dict(name=name)) + return await self._channel.send( + "createSelectorForTest", self._frame._timeout, dict(name=name) + ) def as_element(self) -> Optional["ElementHandle"]: return self async def owner_frame(self) -> Optional["Frame"]: - return from_nullable_channel(await self._channel.send("ownerFrame")) + return from_nullable_channel(await self._channel.send("ownerFrame", None)) async def content_frame(self) -> Optional["Frame"]: - return from_nullable_channel(await self._channel.send("contentFrame")) + return from_nullable_channel(await self._channel.send("contentFrame", None)) async def get_attribute(self, name: str) -> Optional[str]: - return await self._channel.send("getAttribute", dict(name=name)) + return await self._channel.send("getAttribute", None, dict(name=name)) async def text_content(self) -> Optional[str]: - return await self._channel.send("textContent") + return await self._channel.send("textContent", None) async def inner_text(self) -> str: - return await self._channel.send("innerText") + return await self._channel.send("innerText", None) async def inner_html(self) -> str: - return await self._channel.send("innerHTML") + return await self._channel.send("innerHTML", None) async def is_checked(self) -> bool: - return await self._channel.send("isChecked") + return await self._channel.send("isChecked", None) async def is_disabled(self) -> bool: - return await self._channel.send("isDisabled") + return await self._channel.send("isDisabled", None) async def is_editable(self) -> bool: - return await self._channel.send("isEditable") + return await self._channel.send("isEditable", None) async def is_enabled(self) -> bool: - return await self._channel.send("isEnabled") + return await self._channel.send("isEnabled", None) async def is_hidden(self) -> bool: - return await self._channel.send("isHidden") + return await self._channel.send("isHidden", None) async def is_visible(self) -> bool: - return await self._channel.send("isVisible") + return await self._channel.send("isVisible", None) async def dispatch_event(self, type: str, eventInit: Dict = None) -> None: await self._channel.send( - "dispatchEvent", dict(type=type, eventInit=serialize_argument(eventInit)) + "dispatchEvent", + None, + dict(type=type, eventInit=serialize_argument(eventInit)), ) async def scroll_into_view_if_needed(self, timeout: float = None) -> None: - await self._channel.send("scrollIntoViewIfNeeded", locals_to_params(locals())) + await self._channel.send( + "scrollIntoViewIfNeeded", self._frame._timeout, locals_to_params(locals()) + ) async def hover( self, @@ -115,7 +123,9 @@ async def hover( force: bool = None, trial: bool = None, ) -> None: - await self._channel.send("hover", locals_to_params(locals())) + await self._channel.send( + "hover", self._frame._timeout, locals_to_params(locals()) + ) async def click( self, @@ -128,8 +138,11 @@ async def click( force: bool = None, noWaitAfter: bool = None, trial: bool = None, + steps: int = None, ) -> None: - await self._channel.send("click", locals_to_params(locals())) + await self._channel.send( + "click", self._frame._timeout, locals_to_params(locals()) + ) async def dblclick( self, @@ -141,8 +154,11 @@ async def dblclick( force: bool = None, noWaitAfter: bool = None, trial: bool = None, + steps: int = None, ) -> None: - await self._channel.send("dblclick", locals_to_params(locals())) + await self._channel.send( + "dblclick", self._frame._timeout, locals_to_params(locals()) + ) async def select_option( self, @@ -161,7 +177,7 @@ async def select_option( **convert_select_option_values(value, index, label, element), ) ) - return await self._channel.send("selectOption", params) + return await self._channel.send("selectOption", self._frame._timeout, params) async def tap( self, @@ -172,7 +188,9 @@ async def tap( noWaitAfter: bool = None, trial: bool = None, ) -> None: - await self._channel.send("tap", locals_to_params(locals())) + await self._channel.send( + "tap", self._frame._timeout, locals_to_params(locals()) + ) async def fill( self, @@ -181,13 +199,19 @@ async def fill( noWaitAfter: bool = None, force: bool = None, ) -> None: - await self._channel.send("fill", locals_to_params(locals())) + await self._channel.send( + "fill", self._frame._timeout, locals_to_params(locals()) + ) async def select_text(self, force: bool = None, timeout: float = None) -> None: - await self._channel.send("selectText", locals_to_params(locals())) + await self._channel.send( + "selectText", self._frame._timeout, locals_to_params(locals()) + ) async def input_value(self, timeout: float = None) -> str: - return await self._channel.send("inputValue", locals_to_params(locals())) + return await self._channel.send( + "inputValue", self._frame._timeout, locals_to_params(locals()) + ) async def set_input_files( self, @@ -203,6 +227,7 @@ async def set_input_files( converted = await convert_input_files(files, frame.page.context) await self._channel.send( "setInputFiles", + self._frame._timeout, { "timeout": timeout, **converted, @@ -210,7 +235,7 @@ async def set_input_files( ) async def focus(self) -> None: - await self._channel.send("focus") + await self._channel.send("focus", None) async def type( self, @@ -219,7 +244,9 @@ async def type( timeout: float = None, noWaitAfter: bool = None, ) -> None: - await self._channel.send("type", locals_to_params(locals())) + await self._channel.send( + "type", self._frame._timeout, locals_to_params(locals()) + ) async def press( self, @@ -228,7 +255,9 @@ async def press( timeout: float = None, noWaitAfter: bool = None, ) -> None: - await self._channel.send("press", locals_to_params(locals())) + await self._channel.send( + "press", self._frame._timeout, locals_to_params(locals()) + ) async def set_checked( self, @@ -262,7 +291,9 @@ async def check( noWaitAfter: bool = None, trial: bool = None, ) -> None: - await self._channel.send("check", locals_to_params(locals())) + await self._channel.send( + "check", self._frame._timeout, locals_to_params(locals()) + ) async def uncheck( self, @@ -272,10 +303,12 @@ async def uncheck( noWaitAfter: bool = None, trial: bool = None, ) -> None: - await self._channel.send("uncheck", locals_to_params(locals())) + await self._channel.send( + "uncheck", self._frame._timeout, locals_to_params(locals()) + ) async def bounding_box(self) -> Optional[FloatRect]: - return await self._channel.send("boundingBox") + return await self._channel.send("boundingBox", None) async def screenshot( self, @@ -293,6 +326,8 @@ async def screenshot( ) -> bytes: params = locals_to_params(locals()) if "path" in params: + if "type" not in params: + params["type"] = determine_screenshot_type(params["path"]) del params["path"] if "mask" in params: params["mask"] = list( @@ -306,7 +341,9 @@ async def screenshot( params["mask"], ) ) - encoded_binary = await self._channel.send("screenshot", params) + encoded_binary = await self._channel.send( + "screenshot", self._frame._timeout, params + ) decoded_binary = base64.b64decode(encoded_binary) if path: make_dirs_for_file(path) @@ -315,14 +352,16 @@ async def screenshot( async def query_selector(self, selector: str) -> Optional["ElementHandle"]: return from_nullable_channel( - await self._channel.send("querySelector", dict(selector=selector)) + await self._channel.send("querySelector", None, dict(selector=selector)) ) async def query_selector_all(self, selector: str) -> List["ElementHandle"]: return list( map( cast(Callable[[Any], Any], from_nullable_channel), - await self._channel.send("querySelectorAll", dict(selector=selector)), + await self._channel.send( + "querySelectorAll", None, dict(selector=selector) + ), ) ) @@ -335,6 +374,7 @@ async def eval_on_selector( return parse_result( await self._channel.send( "evalOnSelector", + None, dict( selector=selector, expression=expression, @@ -352,6 +392,7 @@ async def eval_on_selector_all( return parse_result( await self._channel.send( "evalOnSelectorAll", + None, dict( selector=selector, expression=expression, @@ -367,7 +408,9 @@ async def wait_for_element_state( ], timeout: float = None, ) -> None: - await self._channel.send("waitForElementState", locals_to_params(locals())) + await self._channel.send( + "waitForElementState", self._frame._timeout, locals_to_params(locals()) + ) async def wait_for_selector( self, @@ -377,7 +420,9 @@ async def wait_for_selector( strict: bool = None, ) -> Optional["ElementHandle"]: return from_nullable_channel( - await self._channel.send("waitForSelector", locals_to_params(locals())) + await self._channel.send( + "waitForSelector", self._frame._timeout, locals_to_params(locals()) + ) ) @@ -410,3 +455,12 @@ def convert_select_option_values( elements = list(map(lambda e: e._channel, element)) return dict(options=options, elements=elements) + + +def determine_screenshot_type(path: Union[str, Path]) -> Literal["jpeg", "png"]: + mime_type, _ = mimetypes.guess_type(path) + if mime_type == "image/png": + return "png" + if mime_type == "image/jpeg": + return "jpeg" + raise Error(f'Unsupported screenshot mime type for path "{path}": {mime_type}') diff --git a/playwright/_impl/_fetch.py b/playwright/_impl/_fetch.py index b53e4e629..50bf4ad4a 100644 --- a/playwright/_impl/_fetch.py +++ b/playwright/_impl/_fetch.py @@ -36,6 +36,7 @@ Error, NameValue, TargetClosedError, + TimeoutSettings, async_readfile, async_writefile, is_file_payload, @@ -74,6 +75,7 @@ async def new_context( storageState: Union[StorageState, str, Path] = None, clientCertificates: List[ClientCertificate] = None, failOnStatusCode: bool = None, + maxRedirects: int = None, ) -> "APIRequestContext": params = locals_to_params(locals()) if "storageState" in params: @@ -89,8 +91,11 @@ async def new_context( ) context = cast( APIRequestContext, - from_channel(await self.playwright._channel.send("newRequest", params)), + from_channel( + await self.playwright._channel.send("newRequest", None, params) + ), ) + context._timeout_settings.set_default_timeout(timeout) return context @@ -101,11 +106,12 @@ def __init__( super().__init__(parent, type, guid, initializer) self._tracing: Tracing = from_channel(initializer["tracing"]) self._close_reason: Optional[str] = None + self._timeout_settings = TimeoutSettings(None) async def dispose(self, reason: str = None) -> None: self._close_reason = reason try: - await self._channel.send("dispose", {"reason": reason}) + await self._channel.send("dispose", None, {"reason": reason}) except Error as e: if is_target_closed_error(e): return @@ -403,8 +409,10 @@ async def _inner_fetch( response = await self._channel.send( "fetch", + self._timeout_settings.timeout, { "url": url, + "timeout": timeout, "params": object_to_array(params) if isinstance(params, dict) else None, "encodedParams": params if isinstance(params, str) else None, "method": method, @@ -413,7 +421,6 @@ async def _inner_fetch( "jsonData": json_data, "formData": form_data, "multipartData": multipart_data, - "timeout": timeout, "failOnStatusCode": failOnStatusCode, "ignoreHTTPSErrors": ignoreHTTPSErrors, "maxRedirects": maxRedirects, @@ -428,7 +435,7 @@ async def storage_state( indexedDB: bool = None, ) -> StorageState: result = await self._channel.send_return_as_dict( - "storageState", {"indexedDB": indexedDB} + "storageState", None, {"indexedDB": indexedDB} ) if path: await async_writefile(path, json.dumps(result)) @@ -483,6 +490,7 @@ async def body(self) -> bytes: result = await self._request._connection.wrap_api_call( lambda: self._request._channel.send_return_as_dict( "fetchResponseBody", + None, { "fetchUid": self._fetch_uid, }, @@ -508,6 +516,7 @@ async def json(self) -> Any: async def dispose(self) -> None: await self._request._channel.send( "disposeAPIResponse", + None, { "fetchUid": self._fetch_uid, }, @@ -520,6 +529,7 @@ def _fetch_uid(self) -> str: async def _fetch_log(self) -> List[str]: return await self._request._channel.send( "fetchLog", + None, { "fetchUid": self._fetch_uid, }, diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index d616046e6..b976667e7 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -19,6 +19,7 @@ Any, Dict, List, + Literal, Optional, Pattern, Sequence, @@ -29,7 +30,13 @@ from pyee import EventEmitter -from playwright._impl._api_structures import AriaRole, FilePayload, Position +from playwright._impl._api_structures import ( + AriaRole, + FilePayload, + FrameExpectOptions, + FrameExpectResult, + Position, +) from playwright._impl._connection import ( ChannelOwner, from_channel, @@ -42,8 +49,8 @@ DocumentLoadState, FrameNavigatedEvent, KeyboardModifier, - Literal, MouseButton, + TimeoutSettings, URLMatch, async_readfile, locals_to_params, @@ -55,6 +62,7 @@ Serializable, add_source_url_to_script, parse_result, + parse_value, serialize_argument, ) from playwright._impl._locator import ( @@ -125,7 +133,7 @@ def _on_frame_navigated(self, event: FrameNavigatedEvent) -> None: self._page.emit("framenavigated", self) async def _query_count(self, selector: str) -> int: - return await self._channel.send("queryCount", {"selector": selector}) + return await self._channel.send("queryCount", None, {"selector": selector}) @property def page(self) -> "Page": @@ -142,7 +150,9 @@ async def goto( return cast( Optional[Response], from_nullable_channel( - await self._channel.send("goto", locals_to_params(locals())) + await self._channel.send( + "goto", self._navigation_timeout, locals_to_params(locals()) + ) ), ) @@ -163,11 +173,33 @@ def _setup_navigation_waiter(self, wait_name: str, timeout: float = None) -> Wai Error("Navigating frame was detached!"), lambda frame: frame == self, ) - if timeout is None: - timeout = self._page._timeout_settings.navigation_timeout() + timeout = self._page._timeout_settings.navigation_timeout(timeout) waiter.reject_on_timeout(timeout, f"Timeout {timeout}ms exceeded.") return waiter + async def _expect( + self, + selector: Optional[str], + expression: str, + options: FrameExpectOptions, + title: str = None, + ) -> FrameExpectResult: + if "expectedValue" in options: + options["expectedValue"] = serialize_argument(options["expectedValue"]) + result = await self._channel.send_return_as_dict( + "expect", + self._timeout, + { + "selector": selector, + "expression": expression, + **options, + }, + title=title, + ) + if result.get("received"): + result["received"] = parse_value(result["received"]) + return result + def expect_navigation( self, url: URLMatch = None, @@ -192,7 +224,7 @@ def predicate(event: Any) -> bool: return True waiter.log(f' navigated to "{event["url"]}"') return url_matches( - cast("Page", self._page)._browser_context._options.get("baseURL"), + cast("Page", self._page)._browser_context._base_url, event["url"], url, ) @@ -225,9 +257,7 @@ async def wait_for_url( timeout: float = None, ) -> None: assert self._page - if url_matches( - self._page._browser_context._options.get("baseURL"), self.url, url - ): + if url_matches(self._page._browser_context._base_url, self.url, url): await self._wait_for_load_state_impl(state=waitUntil, timeout=timeout) return async with self.expect_navigation( @@ -270,13 +300,26 @@ def handle_load_state_event(actual_state: str) -> bool: ) await waiter.result() + def _timeout(self, timeout: Optional[float]) -> float: + timeout_settings = ( + self._page._timeout_settings if self._page else TimeoutSettings(None) + ) + return timeout_settings.timeout(timeout) + + def _navigation_timeout(self, timeout: Optional[float]) -> float: + timeout_settings = ( + self._page._timeout_settings if self._page else TimeoutSettings(None) + ) + return timeout_settings.navigation_timeout(timeout) + async def frame_element(self) -> ElementHandle: - return from_channel(await self._channel.send("frameElement")) + return from_channel(await self._channel.send("frameElement", None)) async def evaluate(self, expression: str, arg: Serializable = None) -> Any: return parse_result( await self._channel.send( "evaluateExpression", + None, dict( expression=expression, arg=serialize_argument(arg), @@ -290,6 +333,7 @@ async def evaluate_handle( return from_channel( await self._channel.send( "evaluateExpressionHandle", + None, dict( expression=expression, arg=serialize_argument(arg), @@ -301,14 +345,16 @@ async def query_selector( self, selector: str, strict: bool = None ) -> Optional[ElementHandle]: return from_nullable_channel( - await self._channel.send("querySelector", locals_to_params(locals())) + await self._channel.send("querySelector", None, locals_to_params(locals())) ) async def query_selector_all(self, selector: str) -> List[ElementHandle]: return list( map( from_channel, - await self._channel.send("querySelectorAll", dict(selector=selector)), + await self._channel.send( + "querySelectorAll", None, dict(selector=selector) + ), ) ) @@ -320,38 +366,48 @@ async def wait_for_selector( state: Literal["attached", "detached", "hidden", "visible"] = None, ) -> Optional[ElementHandle]: return from_nullable_channel( - await self._channel.send("waitForSelector", locals_to_params(locals())) + await self._channel.send( + "waitForSelector", self._timeout, locals_to_params(locals()) + ) ) async def is_checked( self, selector: str, strict: bool = None, timeout: float = None ) -> bool: - return await self._channel.send("isChecked", locals_to_params(locals())) + return await self._channel.send( + "isChecked", self._timeout, locals_to_params(locals()) + ) async def is_disabled( self, selector: str, strict: bool = None, timeout: float = None ) -> bool: - return await self._channel.send("isDisabled", locals_to_params(locals())) + return await self._channel.send( + "isDisabled", self._timeout, locals_to_params(locals()) + ) async def is_editable( self, selector: str, strict: bool = None, timeout: float = None ) -> bool: - return await self._channel.send("isEditable", locals_to_params(locals())) + return await self._channel.send( + "isEditable", self._timeout, locals_to_params(locals()) + ) async def is_enabled( self, selector: str, strict: bool = None, timeout: float = None ) -> bool: - return await self._channel.send("isEnabled", locals_to_params(locals())) + return await self._channel.send( + "isEnabled", self._timeout, locals_to_params(locals()) + ) - async def is_hidden( - self, selector: str, strict: bool = None, timeout: float = None - ) -> bool: - return await self._channel.send("isHidden", locals_to_params(locals())) + async def is_hidden(self, selector: str, strict: bool = None) -> bool: + return await self._channel.send( + "isHidden", self._timeout, locals_to_params(locals()) + ) - async def is_visible( - self, selector: str, strict: bool = None, timeout: float = None - ) -> bool: - return await self._channel.send("isVisible", locals_to_params(locals())) + async def is_visible(self, selector: str, strict: bool = None) -> bool: + return await self._channel.send( + "isVisible", self._timeout, locals_to_params(locals()) + ) async def dispatch_event( self, @@ -363,6 +419,7 @@ async def dispatch_event( ) -> None: await self._channel.send( "dispatchEvent", + self._timeout, locals_to_params( dict( selector=selector, @@ -384,6 +441,7 @@ async def eval_on_selector( return parse_result( await self._channel.send( "evalOnSelector", + None, locals_to_params( dict( selector=selector, @@ -404,6 +462,7 @@ async def eval_on_selector_all( return parse_result( await self._channel.send( "evalOnSelectorAll", + None, dict( selector=selector, expression=expression, @@ -413,7 +472,7 @@ async def eval_on_selector_all( ) async def content(self) -> str: - return await self._channel.send("content") + return await self._channel.send("content", None) async def set_content( self, @@ -421,7 +480,9 @@ async def set_content( timeout: float = None, waitUntil: DocumentLoadState = None, ) -> None: - await self._channel.send("setContent", locals_to_params(locals())) + await self._channel.send( + "setContent", self._navigation_timeout, locals_to_params(locals()) + ) @property def name(self) -> str: @@ -455,7 +516,7 @@ async def add_script_tag( (await async_readfile(path)).decode(), path ) del params["path"] - return from_channel(await self._channel.send("addScriptTag", params)) + return from_channel(await self._channel.send("addScriptTag", None, params)) async def add_style_tag( self, url: str = None, path: Union[str, Path] = None, content: str = None @@ -469,7 +530,7 @@ async def add_style_tag( + "*/" ) del params["path"] - return from_channel(await self._channel.send("addStyleTag", params)) + return from_channel(await self._channel.send("addStyleTag", None, params)) async def click( self, @@ -485,7 +546,24 @@ async def click( strict: bool = None, trial: bool = None, ) -> None: - await self._channel.send("click", locals_to_params(locals())) + await self._click(**locals_to_params(locals())) + + async def _click( + self, + selector: str, + modifiers: Sequence[KeyboardModifier] = None, + position: Position = None, + delay: float = None, + button: MouseButton = None, + clickCount: int = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + strict: bool = None, + trial: bool = None, + steps: int = None, + ) -> None: + await self._channel.send("click", self._timeout, locals_to_params(locals())) async def dblclick( self, @@ -500,7 +578,9 @@ async def dblclick( strict: bool = None, trial: bool = None, ) -> None: - await self._channel.send("dblclick", locals_to_params(locals())) + await self._channel.send( + "dblclick", self._timeout, locals_to_params(locals()), title="Double click" + ) async def tap( self, @@ -513,7 +593,7 @@ async def tap( strict: bool = None, trial: bool = None, ) -> None: - await self._channel.send("tap", locals_to_params(locals())) + await self._channel.send("tap", self._timeout, locals_to_params(locals())) async def fill( self, @@ -524,7 +604,19 @@ async def fill( strict: bool = None, force: bool = None, ) -> None: - await self._channel.send("fill", locals_to_params(locals())) + await self._fill(**locals_to_params(locals())) + + async def _fill( + self, + selector: str, + value: str, + timeout: float = None, + noWaitAfter: bool = None, + strict: bool = None, + force: bool = None, + title: str = None, + ) -> None: + await self._channel.send("fill", self._timeout, locals_to_params(locals())) def locator( self, @@ -605,27 +697,35 @@ def frame_locator(self, selector: str) -> FrameLocator: async def focus( self, selector: str, strict: bool = None, timeout: float = None ) -> None: - await self._channel.send("focus", locals_to_params(locals())) + await self._channel.send("focus", self._timeout, locals_to_params(locals())) async def text_content( self, selector: str, strict: bool = None, timeout: float = None ) -> Optional[str]: - return await self._channel.send("textContent", locals_to_params(locals())) + return await self._channel.send( + "textContent", self._timeout, locals_to_params(locals()) + ) async def inner_text( self, selector: str, strict: bool = None, timeout: float = None ) -> str: - return await self._channel.send("innerText", locals_to_params(locals())) + return await self._channel.send( + "innerText", self._timeout, locals_to_params(locals()) + ) async def inner_html( self, selector: str, strict: bool = None, timeout: float = None ) -> str: - return await self._channel.send("innerHTML", locals_to_params(locals())) + return await self._channel.send( + "innerHTML", self._timeout, locals_to_params(locals()) + ) async def get_attribute( self, selector: str, name: str, strict: bool = None, timeout: float = None ) -> Optional[str]: - return await self._channel.send("getAttribute", locals_to_params(locals())) + return await self._channel.send( + "getAttribute", self._timeout, locals_to_params(locals()) + ) async def hover( self, @@ -638,7 +738,7 @@ async def hover( strict: bool = None, trial: bool = None, ) -> None: - await self._channel.send("hover", locals_to_params(locals())) + await self._channel.send("hover", self._timeout, locals_to_params(locals())) async def drag_and_drop( self, @@ -651,8 +751,11 @@ async def drag_and_drop( strict: bool = None, timeout: float = None, trial: bool = None, + steps: int = None, ) -> None: - await self._channel.send("dragAndDrop", locals_to_params(locals())) + await self._channel.send( + "dragAndDrop", self._timeout, locals_to_params(locals()) + ) async def select_option( self, @@ -675,7 +778,7 @@ async def select_option( **convert_select_option_values(value, index, label, element), ) ) - return await self._channel.send("selectOption", params) + return await self._channel.send("selectOption", self._timeout, params) async def input_value( self, @@ -683,7 +786,9 @@ async def input_value( strict: bool = None, timeout: float = None, ) -> str: - return await self._channel.send("inputValue", locals_to_params(locals())) + return await self._channel.send( + "inputValue", self._timeout, locals_to_params(locals()) + ) async def set_input_files( self, @@ -698,10 +803,11 @@ async def set_input_files( converted = await convert_input_files(files, self.page.context) await self._channel.send( "setInputFiles", + self._timeout, { "selector": selector, "strict": strict, - "timeout": timeout, + "timeout": self._timeout(timeout), **converted, }, ) @@ -715,7 +821,7 @@ async def type( timeout: float = None, noWaitAfter: bool = None, ) -> None: - await self._channel.send("type", locals_to_params(locals())) + await self._channel.send("type", self._timeout, locals_to_params(locals())) async def press( self, @@ -726,7 +832,7 @@ async def press( timeout: float = None, noWaitAfter: bool = None, ) -> None: - await self._channel.send("press", locals_to_params(locals())) + await self._channel.send("press", self._timeout, locals_to_params(locals())) async def check( self, @@ -738,7 +844,7 @@ async def check( strict: bool = None, trial: bool = None, ) -> None: - await self._channel.send("check", locals_to_params(locals())) + await self._channel.send("check", self._timeout, locals_to_params(locals())) async def uncheck( self, @@ -750,10 +856,10 @@ async def uncheck( strict: bool = None, trial: bool = None, ) -> None: - await self._channel.send("uncheck", locals_to_params(locals())) + await self._channel.send("uncheck", self._timeout, locals_to_params(locals())) async def wait_for_timeout(self, timeout: float) -> None: - await self._channel.send("waitForTimeout", locals_to_params(locals())) + await self._channel.send("waitForTimeout", None, {"waitTimeout": timeout}) async def wait_for_function( self, @@ -768,10 +874,12 @@ async def wait_for_function( params["arg"] = serialize_argument(arg) if polling is not None and polling != "raf": params["pollingInterval"] = polling - return from_channel(await self._channel.send("waitForFunction", params)) + return from_channel( + await self._channel.send("waitForFunction", self._timeout, params) + ) async def title(self) -> str: - return await self._channel.send("title") + return await self._channel.send("title", None) async def set_checked( self, @@ -804,4 +912,4 @@ async def set_checked( ) async def _highlight(self, selector: str) -> None: - await self._channel.send("highlight", {"selector": selector}) + await self._channel.send("highlight", None, {"selector": selector}) diff --git a/playwright/_impl/_glob.py b/playwright/_impl/_glob.py index 2d899a789..b38826996 100644 --- a/playwright/_impl/_glob.py +++ b/playwright/_impl/_glob.py @@ -11,13 +11,12 @@ # 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 # https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping escaped_chars = {"$", "^", "+", ".", "*", "(", ")", "|", "\\", "?", "{", "}", "[", "]"} -def glob_to_regex(glob: str) -> "re.Pattern[str]": +def glob_to_regex_pattern(glob: str) -> str: tokens = ["^"] in_group = False @@ -29,40 +28,38 @@ def glob_to_regex(glob: str) -> "re.Pattern[str]": tokens.append("\\" + char if char in escaped_chars else char) i += 1 elif c == "*": - before_deep = glob[i - 1] if i > 0 else None + 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 - after_deep = glob[i + 1] if i + 1 < len(glob) else None - is_deep = ( - star_count > 1 - and (before_deep == "/" or before_deep is None) - and (after_deep == "/" or after_deep is None) - ) - if is_deep: - tokens.append("((?:[^/]*(?:/|$))*)") - 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 == "?": - tokens.append(".") - elif c == "[": - tokens.append("[") - elif c == "]": - tokens.append("]") - elif c == "{": + if c == "{": in_group = True tokens.append("(") elif c == "}": in_group = False tokens.append(")") - elif c == "," and in_group: - 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 re.compile("".join(tokens)) + return "".join(tokens) diff --git a/playwright/_impl/_har_router.py b/playwright/_impl/_har_router.py index 33ff37871..1fa1b0433 100644 --- a/playwright/_impl/_har_router.py +++ b/playwright/_impl/_har_router.py @@ -49,7 +49,7 @@ async def create( not_found_action: RouteFromHarNotFoundPolicy, url_matcher: Optional[URLMatch] = None, ) -> "HarRouter": - har_id = await local_utils._channel.send("harOpen", {"file": file}) + har_id = await local_utils._channel.send("harOpen", None, {"file": file}) return HarRouter( local_utils=local_utils, har_id=har_id, @@ -118,5 +118,5 @@ async def add_page_route(self, page: "Page") -> None: def dispose(self) -> None: asyncio.create_task( - self._local_utils._channel.send("harClose", {"harId": self._har_id}) + self._local_utils._channel.send("harClose", None, {"harId": self._har_id}) ) diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py index 2f7ab57b0..1d7e4f67b 100644 --- a/playwright/_impl/_helper.py +++ b/playwright/_impl/_helper.py @@ -29,12 +29,13 @@ Optional, Pattern, Set, + Tuple, TypedDict, TypeVar, Union, cast, ) -from urllib.parse import urljoin, urlparse +from urllib.parse import ParseResult, urljoin, urlparse, urlunparse from playwright._impl._api_structures import NameValue from playwright._impl._errors import ( @@ -44,7 +45,7 @@ is_target_closed_error, rewrite_error, ) -from playwright._impl._glob import glob_to_regex +from playwright._impl._glob import glob_to_regex_pattern from playwright._impl._greenlets import RouteGreenlet from playwright._impl._str_utils import escape_regex_flags @@ -144,31 +145,132 @@ class FrameNavigatedEvent(TypedDict): def url_matches( - base_url: Optional[str], url_string: str, match: Optional[URLMatch] + base_url: Optional[str], + url_string: str, + match: Optional[URLMatch], + websocket_url: bool = None, ) -> bool: if not match: return True - if isinstance(match, str) and match[0] != "*": - # Allow http(s) baseURL to match ws(s) urls. - if ( - base_url - and re.match(r"^https?://", base_url) - and re.match(r"^wss?://", url_string) - ): - base_url = re.sub(r"^http", "ws", base_url) - if base_url: - match = urljoin(base_url, match) - parsed = urlparse(match) - if parsed.path == "": - parsed = parsed._replace(path="/") - match = parsed.geturl() if isinstance(match, str): - match = glob_to_regex(match) + 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] @@ -178,7 +280,21 @@ class HarLookupResult(TypedDict, total=False): body: Optional[str] +DEFAULT_PLAYWRIGHT_TIMEOUT_IN_MILLISECONDS = 30000 +DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT_IN_MILLISECONDS = 180000 +PLAYWRIGHT_MAX_DEADLINE = 2147483647 # 2^31-1 + + class TimeoutSettings: + + @staticmethod + def launch_timeout(timeout: Optional[float] = None) -> float: + return ( + timeout + if timeout is not None + else DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT_IN_MILLISECONDS + ) + def __init__(self, parent: Optional["TimeoutSettings"]) -> None: self._parent = parent self._default_timeout: Optional[float] = None @@ -194,7 +310,7 @@ def timeout(self, timeout: float = None) -> float: return self._default_timeout if self._parent: return self._parent.timeout() - return 30000 + return DEFAULT_PLAYWRIGHT_TIMEOUT_IN_MILLISECONDS def set_default_navigation_timeout( self, navigation_timeout: Optional[float] @@ -207,12 +323,16 @@ def default_navigation_timeout(self) -> Optional[float]: def default_timeout(self) -> Optional[float]: return self._default_timeout - def navigation_timeout(self) -> float: + def navigation_timeout(self, timeout: float = None) -> float: + if timeout is not None: + return timeout if self._default_navigation_timeout is not None: return self._default_navigation_timeout + if self._default_timeout is not None: + return self._default_timeout if self._parent: return self._parent.navigation_timeout() - return 30000 + return DEFAULT_PLAYWRIGHT_TIMEOUT_IN_MILLISECONDS def serialize_error(ex: Exception, tb: Optional[TracebackType]) -> ErrorPayload: diff --git a/playwright/_impl/_input.py b/playwright/_impl/_input.py index a97ba5d11..8a39242ee 100644 --- a/playwright/_impl/_input.py +++ b/playwright/_impl/_input.py @@ -23,19 +23,19 @@ def __init__(self, channel: Channel) -> None: self._dispatcher_fiber = channel._connection._dispatcher_fiber async def down(self, key: str) -> None: - await self._channel.send("keyboardDown", locals_to_params(locals())) + await self._channel.send("keyboardDown", None, locals_to_params(locals())) async def up(self, key: str) -> None: - await self._channel.send("keyboardUp", locals_to_params(locals())) + await self._channel.send("keyboardUp", None, locals_to_params(locals())) async def insert_text(self, text: str) -> None: - await self._channel.send("keyboardInsertText", locals_to_params(locals())) + await self._channel.send("keyboardInsertText", None, locals_to_params(locals())) async def type(self, text: str, delay: float = None) -> None: - await self._channel.send("keyboardType", locals_to_params(locals())) + await self._channel.send("keyboardType", None, locals_to_params(locals())) async def press(self, key: str, delay: float = None) -> None: - await self._channel.send("keyboardPress", locals_to_params(locals())) + await self._channel.send("keyboardPress", None, locals_to_params(locals())) class Mouse: @@ -45,21 +45,34 @@ def __init__(self, channel: Channel) -> None: self._dispatcher_fiber = channel._connection._dispatcher_fiber async def move(self, x: float, y: float, steps: int = None) -> None: - await self._channel.send("mouseMove", locals_to_params(locals())) + await self._channel.send("mouseMove", None, locals_to_params(locals())) async def down( self, button: MouseButton = None, clickCount: int = None, ) -> None: - await self._channel.send("mouseDown", locals_to_params(locals())) + await self._channel.send("mouseDown", None, locals_to_params(locals())) async def up( self, button: MouseButton = None, clickCount: int = None, ) -> None: - await self._channel.send("mouseUp", locals_to_params(locals())) + await self._channel.send("mouseUp", None, locals_to_params(locals())) + + async def _click( + self, + x: float, + y: float, + delay: float = None, + button: MouseButton = None, + clickCount: int = None, + title: str = None, + ) -> None: + await self._channel.send( + "mouseClick", None, locals_to_params(locals()), title=title + ) async def click( self, @@ -69,7 +82,9 @@ async def click( button: MouseButton = None, clickCount: int = None, ) -> None: - await self._channel.send("mouseClick", locals_to_params(locals())) + params = locals() + del params["self"] + await self._click(**params) async def dblclick( self, @@ -78,10 +93,12 @@ async def dblclick( delay: float = None, button: MouseButton = None, ) -> None: - await self.click(x, y, delay=delay, button=button, clickCount=2) + await self._click( + x, y, delay=delay, button=button, clickCount=2, title="Double click" + ) async def wheel(self, deltaX: float, deltaY: float) -> None: - await self._channel.send("mouseWheel", locals_to_params(locals())) + await self._channel.send("mouseWheel", None, locals_to_params(locals())) class Touchscreen: @@ -91,4 +108,4 @@ def __init__(self, channel: Channel) -> None: self._dispatcher_fiber = channel._connection._dispatcher_fiber async def tap(self, x: float, y: float) -> None: - await self._channel.send("touchscreenTap", locals_to_params(locals())) + await self._channel.send("touchscreenTap", None, locals_to_params(locals())) diff --git a/playwright/_impl/_js_handle.py b/playwright/_impl/_js_handle.py index 572d4975e..84ef40d18 100644 --- a/playwright/_impl/_js_handle.py +++ b/playwright/_impl/_js_handle.py @@ -12,9 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +import base64 import collections.abc import datetime import math +import struct import traceback from pathlib import Path from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union @@ -69,6 +71,7 @@ async def evaluate(self, expression: str, arg: Serializable = None) -> Any: return parse_result( await self._channel.send( "evaluateExpression", + None, dict( expression=expression, arg=serialize_argument(arg), @@ -82,6 +85,7 @@ async def evaluate_handle( return from_channel( await self._channel.send( "evaluateExpressionHandle", + None, dict( expression=expression, arg=serialize_argument(arg), @@ -91,13 +95,16 @@ async def evaluate_handle( async def get_property(self, propertyName: str) -> "JSHandle": return from_channel( - await self._channel.send("getProperty", dict(name=propertyName)) + await self._channel.send("getProperty", None, dict(name=propertyName)) ) async def get_properties(self) -> Dict[str, "JSHandle"]: return { prop["name"]: from_channel(prop["value"]) - for prop in await self._channel.send("getPropertyList") + for prop in await self._channel.send( + "getPropertyList", + None, + ) } def as_element(self) -> Optional["ElementHandle"]: @@ -105,13 +112,21 @@ def as_element(self) -> Optional["ElementHandle"]: async def dispose(self) -> None: try: - await self._channel.send("dispose") + await self._channel.send( + "dispose", + None, + ) except Exception as e: if not is_target_closed_error(e): raise e async def json_value(self) -> Any: - return parse_result(await self._channel.send("jsonValue")) + return parse_result( + await self._channel.send( + "jsonValue", + None, + ) + ) def serialize_value( @@ -260,6 +275,56 @@ def parse_value(value: Any, refs: Optional[Dict[int, Any]] = None) -> Any: if "b" in value: return value["b"] + + if "ta" in value: + encoded_bytes = value["ta"]["b"] + decoded_bytes = base64.b64decode(encoded_bytes) + array_type = value["ta"]["k"] + if array_type == "i8": + word_size = 1 + fmt = "b" + elif array_type == "ui8" or array_type == "ui8c": + word_size = 1 + fmt = "B" + elif array_type == "i16": + word_size = 2 + fmt = "h" + elif array_type == "ui16": + word_size = 2 + fmt = "H" + elif array_type == "i32": + word_size = 4 + fmt = "i" + elif array_type == "ui32": + word_size = 4 + fmt = "I" + elif array_type == "f32": + word_size = 4 + fmt = "f" + elif array_type == "f64": + word_size = 8 + fmt = "d" + elif array_type == "bi64": + word_size = 8 + fmt = "q" + elif array_type == "bui64": + word_size = 8 + fmt = "Q" + else: + raise ValueError(f"Unsupported array type: {array_type}") + + byte_len = len(decoded_bytes) + if byte_len % word_size != 0: + raise ValueError( + f"Decoded bytes length {byte_len} is not a multiple of word size {word_size}" + ) + + if byte_len == 0: + return [] + array_len = byte_len // word_size + # "<" denotes little-endian + format_string = f"<{array_len}{fmt}" + return list(struct.unpack(format_string, decoded_bytes)) return value diff --git a/playwright/_impl/_json_pipe.py b/playwright/_impl/_json_pipe.py index 3a6973baf..41973b8c7 100644 --- a/playwright/_impl/_json_pipe.py +++ b/playwright/_impl/_json_pipe.py @@ -36,7 +36,7 @@ def __init__( def request_stop(self) -> None: self._stop_requested = True - self._pipe_channel.send_no_reply("close", {}) + self._pipe_channel.send_no_reply("close", None, {}) def dispose(self) -> None: self.on_error_future.cancel() @@ -74,4 +74,4 @@ async def run(self) -> None: def send(self, message: Dict) -> None: if self._stop_requested: raise Error("Playwright connection closed") - self._pipe_channel.send_no_reply("send", {"message": message}) + self._pipe_channel.send_no_reply("send", None, {"message": message}) diff --git a/playwright/_impl/_local_utils.py b/playwright/_impl/_local_utils.py index 5ea8b644d..c2d2d3fca 100644 --- a/playwright/_impl/_local_utils.py +++ b/playwright/_impl/_local_utils.py @@ -25,18 +25,17 @@ def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) - self._channel.mark_as_internal_type() self.devices = { device["name"]: parse_device_descriptor(device["descriptor"]) for device in initializer["deviceDescriptors"] } async def zip(self, params: Dict) -> None: - await self._channel.send("zip", params) + await self._channel.send("zip", None, params) async def har_open(self, file: str) -> None: params = locals_to_params(locals()) - await self._channel.send("harOpen", params) + await self._channel.send("harOpen", None, params) async def har_lookup( self, @@ -52,27 +51,28 @@ async def har_lookup( params["postData"] = base64.b64encode(params["postData"]).decode() return cast( HarLookupResult, - await self._channel.send_return_as_dict("harLookup", params), + await self._channel.send_return_as_dict("harLookup", None, params), ) async def har_close(self, harId: str) -> None: params = locals_to_params(locals()) - await self._channel.send("harClose", params) + await self._channel.send("harClose", None, params) async def har_unzip(self, zipFile: str, harFile: str) -> None: params = locals_to_params(locals()) - await self._channel.send("harUnzip", params) + await self._channel.send("harUnzip", None, params) async def tracing_started(self, tracesDir: Optional[str], traceName: str) -> str: params = locals_to_params(locals()) - return await self._channel.send("tracingStarted", params) + return await self._channel.send("tracingStarted", None, params) async def trace_discarded(self, stacks_id: str) -> None: - return await self._channel.send("traceDiscarded", {"stacksId": stacks_id}) + return await self._channel.send("traceDiscarded", None, {"stacksId": stacks_id}) def add_stack_to_tracing_no_reply(self, id: int, frames: List[StackFrame]) -> None: self._channel.send_no_reply( "addStackToTracingNoReply", + None, { "callData": { "stack": frames, diff --git a/playwright/_impl/_locator.py b/playwright/_impl/_locator.py index 37b1f9441..2e6a7abed 100644 --- a/playwright/_impl/_locator.py +++ b/playwright/_impl/_locator.py @@ -14,6 +14,7 @@ import json import pathlib +import re from typing import ( TYPE_CHECKING, Any, @@ -47,7 +48,7 @@ monotonic_time, to_impl, ) -from playwright._impl._js_handle import Serializable, parse_value, serialize_argument +from playwright._impl._js_handle import Serializable from playwright._impl._str_utils import ( escape_for_attribute_selector, escape_for_text_selector, @@ -107,7 +108,7 @@ async def _with_element( task: Callable[[ElementHandle, float], Awaitable[T]], timeout: float = None, ) -> T: - timeout = self._frame.page._timeout_settings.timeout(timeout) + timeout = self._frame._timeout(timeout) deadline = (monotonic_time() + timeout) if timeout else 0 handle = await self.element_handle(timeout=timeout) if not handle: @@ -155,9 +156,10 @@ async def click( force: bool = None, noWaitAfter: bool = None, trial: bool = None, + steps: int = None, ) -> None: params = locals_to_params(locals()) - return await self._frame.click(self._selector, strict=True, **params) + return await self._frame._click(self._selector, strict=True, **params) async def dblclick( self, @@ -169,6 +171,7 @@ async def dblclick( force: bool = None, noWaitAfter: bool = None, trial: bool = None, + steps: int = None, ) -> None: params = locals_to_params(locals()) return await self._frame.dblclick(self._selector, strict=True, **params) @@ -217,7 +220,8 @@ async def clear( noWaitAfter: bool = None, force: bool = None, ) -> None: - await self.fill("", timeout=timeout, force=force) + params = locals_to_params(locals()) + await self._frame._fill(self._selector, value="", title="Clear", **params) def locator( self, @@ -336,6 +340,26 @@ def nth(self, index: int) -> "Locator": def content_frame(self) -> "FrameLocator": return FrameLocator(self._frame, self._selector) + def describe(self, description: str) -> "Locator": + return Locator( + self._frame, + f"{self._selector} >> internal:describe={json.dumps(description)}", + ) + + @property + def description(self) -> Optional[str]: + try: + match = re.search( + r' >> internal:describe=("(?:[^"\\]|\\.)*")$', self._selector + ) + if match: + description = json.loads(match.group(1)) + if isinstance(description, str): + return description + except (json.JSONDecodeError, ValueError): + pass + return None + def filter( self, hasText: Union[str, Pattern[str]] = None, @@ -377,6 +401,7 @@ async def focus(self, timeout: float = None) -> None: async def blur(self, timeout: float = None) -> None: await self._frame._channel.send( "blur", + self._frame._timeout, { "selector": self._selector, "strict": True, @@ -406,6 +431,7 @@ async def drag_to( trial: bool = None, sourcePosition: Position = None, targetPosition: Position = None, + steps: int = None, ) -> None: params = locals_to_params(locals()) del params["target"] @@ -494,19 +520,17 @@ async def is_enabled(self, timeout: float = None) -> bool: ) async def is_hidden(self, timeout: float = None) -> bool: - params = locals_to_params(locals()) + # timeout is deprecated and does nothing return await self._frame.is_hidden( self._selector, strict=True, - **params, ) async def is_visible(self, timeout: float = None) -> bool: - params = locals_to_params(locals()) + # timeout is deprecated and does nothing return await self._frame.is_visible( self._selector, strict=True, - **params, ) async def press( @@ -543,6 +567,7 @@ async def screenshot( async def aria_snapshot(self, timeout: float = None) -> str: return await self._frame._channel.send( "ariaSnapshot", + self._frame._timeout, { "selector": self._selector, **locals_to_params(locals()), @@ -711,21 +736,12 @@ async def set_checked( ) async def _expect( - self, expression: str, options: FrameExpectOptions + self, + expression: str, + options: FrameExpectOptions, + title: str = None, ) -> FrameExpectResult: - if "expectedValue" in options: - options["expectedValue"] = serialize_argument(options["expectedValue"]) - result = await self._frame._channel.send_return_as_dict( - "expect", - { - "selector": self._selector, - "expression": expression, - **options, - }, - ) - if result.get("received"): - result["received"] = parse_value(result["received"]) - return result + return await self._frame._expect(self._selector, expression, options, title) async def highlight(self) -> None: await self._frame._highlight(self._selector) diff --git a/playwright/_impl/_network.py b/playwright/_impl/_network.py index 4b15531af..9f2c29f6e 100644 --- a/playwright/_impl/_network.py +++ b/playwright/_impl/_network.py @@ -66,7 +66,7 @@ from playwright._impl._browser_context import BrowserContext from playwright._impl._fetch import APIResponse from playwright._impl._frame import Frame - from playwright._impl._page import Page + from playwright._impl._page import Page, Worker class FallbackOverrideParameters(TypedDict, total=False): @@ -131,7 +131,6 @@ def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) - self._channel.mark_as_internal_type() self._redirected_from: Optional["Request"] = from_nullable_channel( initializer.get("redirectedFrom") ) @@ -185,6 +184,13 @@ def url(self) -> str: def resource_type(self) -> str: return self._initializer["resourceType"] + @property + def service_worker(self) -> Optional["Worker"]: + return cast( + Optional["Worker"], + from_nullable_channel(self._initializer.get("serviceWorker")), + ) + @property def method(self) -> str: return cast(str, self._fallback_overrides.method or self._initializer["method"]) @@ -193,7 +199,10 @@ async def sizes(self) -> RequestSizes: response = await self.response() if not response: raise Error("Unable to fetch sizes for failed request") - return await response._channel.send("sizes") + return await response._channel.send( + "sizes", + None, + ) @property def post_data(self) -> Optional[str]: @@ -227,7 +236,12 @@ def post_data_buffer(self) -> Optional[bytes]: return None async def response(self) -> Optional["Response"]: - return from_nullable_channel(await self._channel.send("response")) + return from_nullable_channel( + await self._channel.send( + "response", + None, + ) + ) @property def frame(self) -> "Frame": @@ -292,7 +306,9 @@ async def _actual_headers(self) -> "RawHeaders": return RawHeaders(serialize_headers(override)) if not self._all_headers_future: self._all_headers_future = asyncio.Future() - headers = await self._channel.send("rawRequestHeaders") + headers = await self._channel.send( + "rawRequestHeaders", None, is_internal=True + ) self._all_headers_future.set_result(RawHeaders(headers)) return await self._all_headers_future @@ -319,7 +335,6 @@ def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) - self._channel.mark_as_internal_type() self._handling_future: Optional[asyncio.Future["bool"]] = None self._context: "BrowserContext" = cast("BrowserContext", None) self._did_throw = False @@ -350,6 +365,7 @@ async def abort(self, errorCode: str = None) -> None: lambda: self._race_with_page_close( self._channel.send( "abort", + None, { "errorCode": errorCode, }, @@ -435,7 +451,7 @@ async def _inner_fulfill( headers["content-length"] = str(length) params["headers"] = serialize_headers(headers) - await self._race_with_page_close(self._channel.send("fulfill", params)) + await self._race_with_page_close(self._channel.send("fulfill", None, params)) async def _handle_route(self, callback: Callable) -> None: self._check_not_handled() @@ -501,6 +517,7 @@ async def _inner_continue(self, is_fallback: bool = False) -> None: await self._race_with_page_close( self._channel.send( "continue", + None, { "url": options.url, "method": options.method, @@ -520,7 +537,7 @@ async def _inner_continue(self, is_fallback: bool = False) -> None: async def _redirected_navigation_request(self, url: str) -> None: await self._handle_route( lambda: self._race_with_page_close( - self._channel.send("redirectNavigationRequest", {"url": url}) + self._channel.send("redirectNavigationRequest", None, {"url": url}) ) ) @@ -530,7 +547,7 @@ async def _race_with_page_close(self, future: Coroutine) -> None: setattr( fut, "__pw_stack__", - getattr(asyncio.current_task(self._loop), "__pw_stack__", inspect.stack()), + getattr(asyncio.current_task(self._loop), "__pw_stack__", inspect.stack(0)), ) target_closed_future = self.request._target_closed_future() await asyncio.wait( @@ -579,6 +596,7 @@ def close(self, code: int = None, reason: str = None) -> None: self._ws._loop, self._ws._channel.send( "closeServer", + None, { "code": code, "reason": reason, @@ -592,7 +610,7 @@ def send(self, message: Union[str, bytes]) -> None: _create_task_and_ignore_exception( self._ws._loop, self._ws._channel.send( - "sendToServer", {"message": message, "isBase64": False} + "sendToServer", None, {"message": message, "isBase64": False} ), ) else: @@ -600,6 +618,7 @@ def send(self, message: Union[str, bytes]) -> None: self._ws._loop, self._ws._channel.send( "sendToServer", + None, {"message": base64.b64encode(message).decode(), "isBase64": True}, ), ) @@ -610,7 +629,6 @@ def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) - self._channel.mark_as_internal_type() self._on_page_message: Optional[Callable[[Union[str, bytes]], Any]] = None self._on_page_close: Optional[Callable[[Optional[int], Optional[str]], Any]] = ( None @@ -636,7 +654,7 @@ def _channel_message_from_page(self, event: Dict) -> None: ) elif self._connected: _create_task_and_ignore_exception( - self._loop, self._channel.send("sendToServer", event) + self._loop, self._channel.send("sendToServer", None, event) ) def _channel_message_from_server(self, event: Dict) -> None: @@ -648,7 +666,7 @@ def _channel_message_from_server(self, event: Dict) -> None: ) else: _create_task_and_ignore_exception( - self._loop, self._channel.send("sendToPage", event) + self._loop, self._channel.send("sendToPage", None, event) ) def _channel_close_page(self, event: Dict) -> None: @@ -656,7 +674,7 @@ def _channel_close_page(self, event: Dict) -> None: self._on_page_close(event["code"], event["reason"]) else: _create_task_and_ignore_exception( - self._loop, self._channel.send("closeServer", event) + self._loop, self._channel.send("closeServer", None, event) ) def _channel_close_server(self, event: Dict) -> None: @@ -664,7 +682,7 @@ def _channel_close_server(self, event: Dict) -> None: self._on_server_close(event["code"], event["reason"]) else: _create_task_and_ignore_exception( - self._loop, self._channel.send("closePage", event) + self._loop, self._channel.send("closePage", None, event) ) @property @@ -674,7 +692,7 @@ def url(self) -> str: async def close(self, code: int = None, reason: str = None) -> None: try: await self._channel.send( - "closePage", {"code": code, "reason": reason, "wasClean": True} + "closePage", None, {"code": code, "reason": reason, "wasClean": True} ) except Exception: pass @@ -683,7 +701,12 @@ def connect_to_server(self) -> "WebSocketRoute": if self._connected: raise Error("Already connected to the server") self._connected = True - asyncio.create_task(self._channel.send("connect")) + asyncio.create_task( + self._channel.send( + "connect", + None, + ) + ) return cast("WebSocketRoute", self._server) def send(self, message: Union[str, bytes]) -> None: @@ -691,7 +714,7 @@ def send(self, message: Union[str, bytes]) -> None: _create_task_and_ignore_exception( self._loop, self._channel.send( - "sendToPage", {"message": message, "isBase64": False} + "sendToPage", None, {"message": message, "isBase64": False} ), ) else: @@ -699,6 +722,7 @@ def send(self, message: Union[str, bytes]) -> None: self._loop, self._channel.send( "sendToPage", + None, { "message": base64.b64encode(message).decode(), "isBase64": True, @@ -716,7 +740,13 @@ async def _after_handle(self) -> None: if self._connected: return # Ensure that websocket is "open" and can send messages without an actual server connection. - await self._channel.send("ensureOpened") + try: + await self._channel.send( + "ensureOpened", + None, + ) + except Exception: + pass class WebSocketRouteHandler: @@ -754,7 +784,7 @@ def prepare_interception_patterns( return patterns def matches(self, ws_url: str) -> bool: - return url_matches(self._base_url, ws_url, self.url) + return url_matches(self._base_url, ws_url, self.url, True) async def handle(self, websocket_route: "WebSocketRoute") -> None: coro_or_future = self.handler(websocket_route) @@ -768,7 +798,6 @@ def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) - self._channel.mark_as_internal_type() self._request: Request = from_channel(self._initializer["request"]) timing = self._initializer["timing"] self._request._timing["startTime"] = timing["startTime"] @@ -830,15 +859,27 @@ async def header_values(self, name: str) -> List[str]: async def _actual_headers(self) -> "RawHeaders": if not self._raw_headers_future: self._raw_headers_future = asyncio.Future() - headers = cast(HeadersArray, await self._channel.send("rawResponseHeaders")) + headers = cast( + HeadersArray, + await self._channel.send( + "rawResponseHeaders", + None, + ), + ) self._raw_headers_future.set_result(RawHeaders(headers)) return await self._raw_headers_future async def server_addr(self) -> Optional[RemoteAddr]: - return await self._channel.send("serverAddr") + return await self._channel.send( + "serverAddr", + None, + ) async def security_details(self) -> Optional[SecurityDetails]: - return await self._channel.send("securityDetails") + return await self._channel.send( + "securityDetails", + None, + ) async def finished(self) -> None: async def on_finished() -> None: @@ -857,7 +898,10 @@ async def on_finished() -> None: await on_finished_task async def body(self) -> bytes: - binary = await self._channel.send("body") + binary = await self._channel.send( + "body", + None, + ) return base64.b64decode(binary) async def text(self) -> str: diff --git a/playwright/_impl/_object_factory.py b/playwright/_impl/_object_factory.py index 5f38b781b..b44009bc3 100644 --- a/playwright/_impl/_object_factory.py +++ b/playwright/_impl/_object_factory.py @@ -35,7 +35,6 @@ ) from playwright._impl._page import BindingCall, Page, Worker from playwright._impl._playwright import Playwright -from playwright._impl._selectors import SelectorsOwner from playwright._impl._stream import Stream from playwright._impl._tracing import Tracing from playwright._impl._writable_stream import WritableStream @@ -100,6 +99,4 @@ def create_remote_object( return Worker(parent, type, guid, initializer) if type == "WritableStream": return WritableStream(parent, type, guid, initializer) - if type == "Selectors": - return SelectorsOwner(parent, type, guid, initializer) return DummyObject(parent, type, guid, initializer) diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 6327cce70..1f05a9048 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -33,7 +33,6 @@ cast, ) -from playwright._impl._accessibility import Accessibility from playwright._impl._api_structures import ( AriaRole, FilePayload, @@ -51,7 +50,7 @@ ) from playwright._impl._console_message import ConsoleMessage from playwright._impl._download import Download -from playwright._impl._element_handle import ElementHandle +from playwright._impl._element_handle import ElementHandle, determine_screenshot_type from playwright._impl._errors import Error, TargetClosedError, is_target_closed_error from playwright._impl._event_context_manager import EventContextManagerImpl from playwright._impl._file_chooser import FileChooser @@ -79,6 +78,7 @@ async_writefile, locals_to_params, make_dirs_for_file, + parse_error, serialize_error, url_matches, ) @@ -149,7 +149,6 @@ class Page(ChannelOwner): WebSocket="websocket", Worker="worker", ) - accessibility: Accessibility keyboard: Keyboard mouse: Mouse touchscreen: Touchscreen @@ -159,7 +158,6 @@ def __init__( ) -> None: super().__init__(parent, type, guid, initializer) self._browser_context = cast("BrowserContext", parent) - self.accessibility = Accessibility(self._channel) self.keyboard = Keyboard(self._channel) self.mouse = Mouse(self._channel) self.touchscreen = Touchscreen(self._channel) @@ -227,6 +225,7 @@ def __init__( ), ) self._channel.on("video", lambda params: self._on_video(params)) + self._channel.on("viewportSizeChanged", self._on_viewport_size_changed) self._channel.on( "webSocket", lambda params: self.emit( @@ -286,7 +285,7 @@ async def _on_route(self, route: Route) -> None: route_handlers = self._routes.copy() for route_handler in route_handlers: # If the page was closed we stall all requests right away. - if self._close_was_called or self.context._close_was_called: + if self._close_was_called or self.context._closing_or_closed: return if not route_handler.matches(route.request.url): continue @@ -343,8 +342,6 @@ def _on_close(self) -> None: self._is_closed = True if self in self._browser_context._pages: self._browser_context._pages.remove(self) - if self in self._browser_context._background_pages: - self._browser_context._background_pages.remove(self) self._dispose_har_routers() self.emit(Page.Events.Close, self) @@ -363,6 +360,9 @@ def _on_video(self, params: Any) -> None: artifact = from_channel(params["artifact"]) self._force_video()._artifact_ready(artifact) + def _on_viewport_size_changed(self, params: Any) -> None: + self._viewport_size = params["viewportSize"] + @property def context(self) -> "BrowserContext": return self._browser_context @@ -384,9 +384,7 @@ def frame(self, name: str = None, url: URLMatch = None) -> Optional[Frame]: for frame in self._frames: if name and frame.name == name: return frame - if url and url_matches( - self._browser_context._options.get("baseURL"), frame.url, url - ): + if url and url_matches(self._browser_context._base_url, frame.url, url): return frame return None @@ -397,13 +395,9 @@ def frames(self) -> List[Frame]: def set_default_navigation_timeout(self, timeout: float) -> None: self._timeout_settings.set_default_navigation_timeout(timeout) - self._channel.send_no_reply( - "setDefaultNavigationTimeoutNoReply", dict(timeout=timeout) - ) def set_default_timeout(self, timeout: float) -> None: self._timeout_settings.set_default_timeout(timeout) - self._channel.send_no_reply("setDefaultTimeoutNoReply", dict(timeout=timeout)) async def query_selector( self, @@ -447,12 +441,14 @@ async def is_enabled( async def is_hidden( self, selector: str, strict: bool = None, timeout: float = None ) -> bool: - return await self._main_frame.is_hidden(**locals_to_params(locals())) + # timeout is deprecated and does nothing + return await self._main_frame.is_hidden(selector=selector, strict=strict) async def is_visible( self, selector: str, strict: bool = None, timeout: float = None ) -> bool: - return await self._main_frame.is_visible(**locals_to_params(locals())) + # timeout is deprecated and does nothing + return await self._main_frame.is_visible(selector=selector, strict=strict) async def dispatch_event( self, @@ -519,12 +515,16 @@ async def expose_binding( ) self._bindings[name] = callback await self._channel.send( - "exposeBinding", dict(name=name, needsHandle=handle or False) + "exposeBinding", + None, + dict(name=name, needsHandle=handle or False), ) async def set_extra_http_headers(self, headers: Dict[str, str]) -> None: await self._channel.send( - "setExtraHTTPHeaders", dict(headers=serialize_headers(headers)) + "setExtraHTTPHeaders", + None, + dict(headers=serialize_headers(headers)), ) @property @@ -557,7 +557,11 @@ async def reload( waitUntil: DocumentLoadState = None, ) -> Optional[Response]: return from_nullable_channel( - await self._channel.send("reload", locals_to_params(locals())) + await self._channel.send( + "reload", + self._timeout_settings.navigation_timeout, + locals_to_params(locals()), + ) ) async def wait_for_load_state( @@ -588,7 +592,11 @@ async def go_back( waitUntil: DocumentLoadState = None, ) -> Optional[Response]: return from_nullable_channel( - await self._channel.send("goBack", locals_to_params(locals())) + await self._channel.send( + "goBack", + self._timeout_settings.navigation_timeout, + locals_to_params(locals()), + ) ) async def go_forward( @@ -597,11 +605,15 @@ async def go_forward( waitUntil: DocumentLoadState = None, ) -> Optional[Response]: return from_nullable_channel( - await self._channel.send("goForward", locals_to_params(locals())) + await self._channel.send( + "goForward", + self._timeout_settings.navigation_timeout, + locals_to_params(locals()), + ) ) async def request_gc(self) -> None: - await self._channel.send("requestGC") + await self._channel.send("requestGC", None) async def emulate_media( self, @@ -630,18 +642,22 @@ async def emulate_media( params["contrast"] = ( "no-override" if params["contrast"] == "null" else contrast ) - await self._channel.send("emulateMedia", params) + await self._channel.send("emulateMedia", None, params) async def set_viewport_size(self, viewportSize: ViewportSize) -> None: self._viewport_size = viewportSize - await self._channel.send("setViewportSize", locals_to_params(locals())) + await self._channel.send( + "setViewportSize", + None, + locals_to_params(locals()), + ) @property def viewport_size(self) -> Optional[ViewportSize]: return self._viewport_size async def bring_to_front(self) -> None: - await self._channel.send("bringToFront") + await self._channel.send("bringToFront", None) async def add_init_script( self, script: str = None, path: Union[str, Path] = None @@ -652,7 +668,7 @@ async def add_init_script( ) if not isinstance(script, str): raise Error("Either path or script parameter must be specified") - await self._channel.send("addInitScript", dict(source=script)) + await self._channel.send("addInitScript", None, dict(source=script)) async def route( self, url: URLMatch, handler: RouteHandlerCallback, times: int = None @@ -660,7 +676,7 @@ async def route( self._routes.insert( 0, RouteHandler( - self._browser_context._options.get("baseURL"), + self._browser_context._base_url, url, handler, True if self._dispatcher_fiber else False, @@ -688,24 +704,21 @@ async def _unroute_internal( behavior: Literal["default", "ignoreErrors", "wait"] = None, ) -> None: self._routes = remaining - await self._update_interception_patterns() - if behavior is None or behavior == "default": - return - await asyncio.gather( - *map( - lambda route: route.stop(behavior), # type: ignore - removed, + if behavior is not None and behavior != "default": + await asyncio.gather( + *map( + lambda route: route.stop(behavior), # type: ignore + removed, + ) ) - ) + await self._update_interception_patterns() async def route_web_socket( self, url: URLMatch, handler: WebSocketRouteHandlerCallback ) -> None: self._web_socket_routes.insert( 0, - WebSocketRouteHandler( - self._browser_context._options.get("baseURL"), url, handler - ), + WebSocketRouteHandler(self._browser_context._base_url, url, handler), ) await self._update_web_socket_interception_patterns() @@ -750,7 +763,9 @@ async def route_from_har( async def _update_interception_patterns(self) -> None: patterns = RouteHandler.prepare_interception_patterns(self._routes) await self._channel.send( - "setNetworkInterceptionPatterns", {"patterns": patterns} + "setNetworkInterceptionPatterns", + None, + {"patterns": patterns}, ) async def _update_web_socket_interception_patterns(self) -> None: @@ -758,7 +773,9 @@ async def _update_web_socket_interception_patterns(self) -> None: self._web_socket_routes ) await self._channel.send( - "setWebSocketInterceptionPatterns", {"patterns": patterns} + "setWebSocketInterceptionPatterns", + None, + {"patterns": patterns}, ) async def screenshot( @@ -779,6 +796,8 @@ async def screenshot( ) -> bytes: params = locals_to_params(locals()) if "path" in params: + if "type" not in params: + params["type"] = determine_screenshot_type(params["path"]) del params["path"] if "mask" in params: params["mask"] = list( @@ -792,7 +811,9 @@ async def screenshot( params["mask"], ) ) - encoded_binary = await self._channel.send("screenshot", params) + encoded_binary = await self._channel.send( + "screenshot", self._timeout_settings.timeout, params + ) decoded_binary = base64.b64decode(encoded_binary) if path: make_dirs_for_file(path) @@ -806,7 +827,7 @@ async def close(self, runBeforeUnload: bool = None, reason: str = None) -> None: self._close_reason = reason self._close_was_called = True try: - await self._channel.send("close", locals_to_params(locals())) + await self._channel.send("close", None, locals_to_params(locals())) if self._owned_context: await self._owned_context.close() except Exception as e: @@ -830,7 +851,7 @@ async def click( trial: bool = None, strict: bool = None, ) -> None: - return await self._main_frame.click(**locals_to_params(locals())) + return await self._main_frame._click(**locals_to_params(locals())) async def dblclick( self, @@ -993,6 +1014,7 @@ async def drag_and_drop( timeout: float = None, strict: bool = None, trial: bool = None, + steps: int = None, ) -> None: return await self._main_frame.drag_and_drop(**locals_to_params(locals())) @@ -1105,7 +1127,9 @@ async def pause(self) -> None: try: await asyncio.wait( [ - asyncio.create_task(self._browser_context._channel.send("pause")), + asyncio.create_task( + self._browser_context._channel.send("pause", None) + ), self._closed_or_crashed_future, ], return_when=asyncio.FIRST_COMPLETED, @@ -1137,7 +1161,7 @@ async def pdf( params = locals_to_params(locals()) if "path" in params: del params["path"] - encoded_binary = await self._channel.send("pdf", params) + encoded_binary = await self._channel.send("pdf", None, params) decoded_binary = base64.b64decode(encoded_binary) if path: make_dirs_for_file(path) @@ -1156,7 +1180,7 @@ def video( # Note: we are creating Video object lazily, because we do not know # BrowserContextOptions when constructing the page - it is assigned # too late during launchPersistentContext. - if not self._browser_context._options.get("recordVideo"): + if not self._browser_context._videos_dir: return None return self._force_video() @@ -1243,7 +1267,7 @@ def expect_request( def my_predicate(request: Request) -> bool: if not callable(urlOrPredicate): return url_matches( - self._browser_context._options.get("baseURL"), + self._browser_context._base_url, request.url, urlOrPredicate, ) @@ -1275,7 +1299,7 @@ def expect_response( def my_predicate(request: Response) -> bool: if not callable(urlOrPredicate): return url_matches( - self._browser_context._options.get("baseURL"), + self._browser_context._base_url, request.url, urlOrPredicate, ) @@ -1347,6 +1371,7 @@ async def add_locator_handler( return uid = await self._channel.send( "registerLocatorHandler", + None, { "selector": locator._selector, "noWaitAfter": noWaitAfter, @@ -1387,7 +1412,9 @@ def _handler() -> None: try: await self._connection.wrap_api_call( lambda: self._channel.send( - "resolveLocatorHandlerNoReply", {"uid": uid, "remove": remove} + "resolveLocatorHandlerNoReply", + None, + {"uid": uid, "remove": remove}, ), is_internal=True, ) @@ -1398,16 +1425,38 @@ async def remove_locator_handler(self, locator: "Locator") -> None: for uid, data in self._locator_handlers.copy().items(): if data.locator._equals(locator): del self._locator_handlers[uid] - self._channel.send_no_reply("unregisterLocatorHandler", {"uid": uid}) + self._channel.send_no_reply( + "unregisterLocatorHandler", + None, + {"uid": uid}, + ) + + async def requests(self) -> List[Request]: + request_objects = await self._channel.send("requests", None) + return [from_channel(r) for r in request_objects] + + async def console_messages(self) -> List[ConsoleMessage]: + message_dicts = await self._channel.send("consoleMessages", None) + return [ + ConsoleMessage( + {**event, "page": self._channel}, self._loop, self._dispatcher_fiber + ) + for event in message_dicts + ] + + async def page_errors(self) -> List[Error]: + error_objects = await self._channel.send("pageErrors", None) + return [parse_error(error["error"]) for error in error_objects] class Worker(ChannelOwner): - Events = SimpleNamespace(Close="close") + Events = SimpleNamespace(Close="close", Console="console") def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) + self._set_event_to_subscription_mapping({Worker.Events.Console: "console"}) self._channel.on("close", lambda _: self._on_close()) self._page: Optional[Page] = None self._context: Optional["BrowserContext"] = None @@ -1430,6 +1479,7 @@ async def evaluate(self, expression: str, arg: Serializable = None) -> Any: return parse_result( await self._channel.send( "evaluateExpression", + None, dict( expression=expression, arg=serialize_argument(arg), @@ -1443,6 +1493,7 @@ async def evaluate_handle( return from_channel( await self._channel.send( "evaluateExpressionHandle", + None, dict( expression=expression, arg=serialize_argument(arg), @@ -1450,6 +1501,31 @@ async def evaluate_handle( ) ) + def expect_event( + self, + event: str, + predicate: Callable = None, + timeout: float = None, + ) -> EventContextManagerImpl: + if timeout is None: + if self._page: + timeout = self._page._timeout_settings.timeout() + elif self._context: + timeout = self._context._timeout_settings.timeout() + else: + timeout = 30000 + waiter = Waiter(self, f"worker.expect_event({event})") + waiter.reject_on_timeout( + cast(float, timeout), + f'Timeout {timeout}ms exceeded while waiting for event "{event}"', + ) + if event != Worker.Events.Close: + waiter.reject_on_event( + self, Worker.Events.Close, lambda: TargetClosedError() + ) + waiter.wait_for_event(self, event, predicate) + return EventContextManagerImpl(waiter.result()) + class BindingCall(ChannelOwner): def __init__( @@ -1468,12 +1544,14 @@ async def call(self, func: Callable) -> None: result = func(source, *func_args) if inspect.iscoroutine(result): result = await result - await self._channel.send("resolve", dict(result=serialize_argument(result))) + await self._channel.send( + "resolve", None, dict(result=serialize_argument(result)) + ) except Exception as e: tb = sys.exc_info()[2] asyncio.create_task( self._channel.send( - "reject", dict(error=dict(error=serialize_error(e, tb))) + "reject", None, dict(error=dict(error=serialize_error(e, tb))) ) ) diff --git a/playwright/_impl/_path_utils.py b/playwright/_impl/_path_utils.py index 267a82ab0..b405a0675 100644 --- a/playwright/_impl/_path_utils.py +++ b/playwright/_impl/_path_utils.py @@ -14,12 +14,14 @@ import inspect from pathlib import Path +from types import FrameType +from typing import cast def get_file_dirname() -> Path: """Returns the callee (`__file__`) directory name""" - frame = inspect.stack()[1] - module = inspect.getmodule(frame[0]) + frame = cast(FrameType, inspect.currentframe()).f_back + module = inspect.getmodule(frame) assert module assert module.__file__ return Path(module.__file__).parent.absolute() diff --git a/playwright/_impl/_playwright.py b/playwright/_impl/_playwright.py index c02e73316..5c0151158 100644 --- a/playwright/_impl/_playwright.py +++ b/playwright/_impl/_playwright.py @@ -17,7 +17,7 @@ from playwright._impl._browser_type import BrowserType from playwright._impl._connection import ChannelOwner, from_channel from playwright._impl._fetch import APIRequest -from playwright._impl._selectors import Selectors, SelectorsOwner +from playwright._impl._selectors import Selectors class Playwright(ChannelOwner): @@ -41,12 +41,7 @@ def __init__( self.webkit._playwright = self self.selectors = Selectors(self._loop, self._dispatcher_fiber) - selectors_owner: SelectorsOwner = from_channel(initializer["selectors"]) - self.selectors._add_channel(selectors_owner) - self._connection.on( - "close", lambda: self.selectors._remove_channel(selectors_owner) - ) self.devices = self._connection.local_utils.devices def __getitem__(self, value: str) -> "BrowserType": @@ -59,10 +54,7 @@ def __getitem__(self, value: str) -> "BrowserType": raise ValueError("Invalid browser " + value) def _set_selectors(self, selectors: Selectors) -> None: - selectors_owner = from_channel(self._initializer["selectors"]) - self.selectors._remove_channel(selectors_owner) self.selectors = selectors - self.selectors._add_channel(selectors_owner) async def stop(self) -> None: pass diff --git a/playwright/_impl/_selectors.py b/playwright/_impl/_selectors.py index cf8af8c06..c3bac78e5 100644 --- a/playwright/_impl/_selectors.py +++ b/playwright/_impl/_selectors.py @@ -14,20 +14,21 @@ import asyncio from pathlib import Path -from typing import Any, Dict, List, Set, Union +from typing import Any, Dict, List, Optional, Set, Union -from playwright._impl._connection import ChannelOwner +from playwright._impl._browser_context import BrowserContext from playwright._impl._errors import Error from playwright._impl._helper import async_readfile -from playwright._impl._locator import set_test_id_attribute_name, test_id_attribute_name +from playwright._impl._locator import set_test_id_attribute_name class Selectors: def __init__(self, loop: asyncio.AbstractEventLoop, dispatcher_fiber: Any) -> None: self._loop = loop - self._channels: Set[SelectorsOwner] = set() - self._registrations: List[Dict] = [] + self._contexts_for_selectors: Set[BrowserContext] = set() + self._selector_engines: List[Dict] = [] self._dispatcher_fiber = dispatcher_fiber + self._test_id_attribute_name: Optional[str] = None async def register( self, @@ -36,41 +37,31 @@ async def register( path: Union[str, Path] = None, contentScript: bool = None, ) -> None: + if any(engine for engine in self._selector_engines if engine["name"] == name): + raise Error( + f'Selectors.register: "{name}" selector engine has been already registered' + ) if not script and not path: raise Error("Either source or path should be specified") if path: script = (await async_readfile(path)).decode() - params: Dict[str, Any] = dict(name=name, source=script) + engine: Dict[str, Any] = dict(name=name, source=script) if contentScript: - params["contentScript"] = True - for channel in self._channels: - await channel._channel.send("register", params) - self._registrations.append(params) + engine["contentScript"] = contentScript + for context in self._contexts_for_selectors: + await context._channel.send( + "registerSelectorEngine", + None, + {"selectorEngine": engine}, + ) + self._selector_engines.append(engine) def set_test_id_attribute(self, attributeName: str) -> None: set_test_id_attribute_name(attributeName) - for channel in self._channels: - channel._channel.send_no_reply( - "setTestIdAttributeName", {"testIdAttributeName": attributeName} - ) - - def _add_channel(self, channel: "SelectorsOwner") -> None: - self._channels.add(channel) - for params in self._registrations: - # This should not fail except for connection closure, but just in case we catch. - channel._channel.send_no_reply("register", params) - channel._channel.send_no_reply( + self._test_id_attribute_name = attributeName + for context in self._contexts_for_selectors: + context._channel.send_no_reply( "setTestIdAttributeName", - {"testIdAttributeName": test_id_attribute_name()}, + None, + {"testIdAttributeName": attributeName}, ) - - def _remove_channel(self, channel: "SelectorsOwner") -> None: - if channel in self._channels: - self._channels.remove(channel) - - -class SelectorsOwner(ChannelOwner): - def __init__( - self, parent: ChannelOwner, type: str, guid: str, initializer: Dict - ) -> None: - super().__init__(parent, type, guid, initializer) diff --git a/playwright/_impl/_set_input_files_helpers.py b/playwright/_impl/_set_input_files_helpers.py index ababf5fab..65307e0a2 100644 --- a/playwright/_impl/_set_input_files_helpers.py +++ b/playwright/_impl/_set_input_files_helpers.py @@ -14,6 +14,7 @@ import base64 import collections.abc import os +import stat from pathlib import Path from typing import ( TYPE_CHECKING, @@ -84,6 +85,7 @@ async def convert_input_files( result = await context._connection.wrap_api_call( lambda: context._channel.send_return_as_dict( "createTempFiles", + None, { "rootDirName": ( os.path.basename(local_directory) @@ -138,12 +140,13 @@ async def convert_input_files( def resolve_paths_and_directory_for_input_files( - items: Sequence[Union[str, Path]] + items: Sequence[Union[str, Path]], ) -> Tuple[Optional[List[str]], Optional[str]]: local_paths: Optional[List[str]] = None local_directory: Optional[str] = None for item in items: - if os.path.isdir(item): + item_stat = os.stat(item) # Raises FileNotFoundError if doesn't exist + if stat.S_ISDIR(item_stat.st_mode): if local_directory: raise Error("Multiple directories are not supported") local_directory = str(Path(item).resolve()) diff --git a/playwright/_impl/_stream.py b/playwright/_impl/_stream.py index d27427589..04afa48e1 100644 --- a/playwright/_impl/_stream.py +++ b/playwright/_impl/_stream.py @@ -28,7 +28,7 @@ def __init__( async def save_as(self, path: Union[str, Path]) -> None: file = await self._loop.run_in_executor(None, lambda: open(path, "wb")) while True: - binary = await self._channel.send("read", {"size": 1024 * 1024}) + binary = await self._channel.send("read", None, {"size": 1024 * 1024}) if not binary: break await self._loop.run_in_executor( @@ -39,7 +39,7 @@ async def save_as(self, path: Union[str, Path]) -> None: async def read_all(self) -> bytes: binary = b"" while True: - chunk = await self._channel.send("read", {"size": 1024 * 1024}) + chunk = await self._channel.send("read", None, {"size": 1024 * 1024}) if not chunk: break binary += base64.b64decode(chunk) diff --git a/playwright/_impl/_sync_base.py b/playwright/_impl/_sync_base.py index b50c7479d..3fef433b5 100644 --- a/playwright/_impl/_sync_base.py +++ b/playwright/_impl/_sync_base.py @@ -105,8 +105,8 @@ def _sync( g_self = greenlet.getcurrent() task: asyncio.tasks.Task[Any] = self._loop.create_task(coro) - setattr(task, "__pw_stack__", inspect.stack()) - setattr(task, "__pw_stack_trace__", traceback.extract_stack()) + setattr(task, "__pw_stack__", inspect.stack(0)) + setattr(task, "__pw_stack_trace__", traceback.extract_stack(limit=10)) task.add_done_callback(lambda _: g_self.switch()) while not task.done(): @@ -142,9 +142,9 @@ def __enter__(self: Self) -> Self: def __exit__( self, - exc_type: Type[BaseException], - exc_val: BaseException, - _traceback: TracebackType, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + _traceback: Optional[TracebackType], ) -> None: self.close() diff --git a/playwright/_impl/_tracing.py b/playwright/_impl/_tracing.py index a68b53bf7..bbc6ec35e 100644 --- a/playwright/_impl/_tracing.py +++ b/playwright/_impl/_tracing.py @@ -26,7 +26,6 @@ def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) - self._channel.mark_as_internal_type() self._include_sources: bool = False self._stacks_id: Optional[str] = None self._is_tracing: bool = False @@ -43,15 +42,15 @@ async def start( params = locals_to_params(locals()) self._include_sources = bool(sources) - await self._channel.send("tracingStart", params) + await self._channel.send("tracingStart", None, params) trace_name = await self._channel.send( - "tracingStartChunk", {"title": title, "name": name} + "tracingStartChunk", None, {"title": title, "name": name} ) await self._start_collecting_stacks(trace_name) async def start_chunk(self, title: str = None, name: str = None) -> None: params = locals_to_params(locals()) - trace_name = await self._channel.send("tracingStartChunk", params) + trace_name = await self._channel.send("tracingStartChunk", None, params) await self._start_collecting_stacks(trace_name) async def _start_collecting_stacks(self, trace_name: str) -> None: @@ -67,14 +66,17 @@ async def stop_chunk(self, path: Union[pathlib.Path, str] = None) -> None: async def stop(self, path: Union[pathlib.Path, str] = None) -> None: await self._do_stop_chunk(path) - await self._channel.send("tracingStop") + await self._channel.send( + "tracingStop", + None, + ) async def _do_stop_chunk(self, file_path: Union[pathlib.Path, str] = None) -> None: self._reset_stack_counter() if not file_path: # Not interested in any artifacts - await self._channel.send("tracingStopChunk", {"mode": "discard"}) + await self._channel.send("tracingStopChunk", None, {"mode": "discard"}) if self._stacks_id: await self._connection.local_utils.trace_discarded(self._stacks_id) return @@ -83,7 +85,7 @@ async def _do_stop_chunk(self, file_path: Union[pathlib.Path, str] = None) -> No if is_local: result = await self._channel.send_return_as_dict( - "tracingStopChunk", {"mode": "entries"} + "tracingStopChunk", None, {"mode": "entries"} ) await self._connection.local_utils.zip( { @@ -98,6 +100,7 @@ async def _do_stop_chunk(self, file_path: Union[pathlib.Path, str] = None) -> No result = await self._channel.send_return_as_dict( "tracingStopChunk", + None, { "mode": "archive", }, @@ -134,7 +137,10 @@ def _reset_stack_counter(self) -> None: self._connection.set_is_tracing(False) async def group(self, name: str, location: TracingGroupLocation = None) -> None: - await self._channel.send("tracingGroup", locals_to_params(locals())) + await self._channel.send("tracingGroup", None, locals_to_params(locals())) async def group_end(self) -> None: - await self._channel.send("tracingGroupEnd") + await self._channel.send( + "tracingGroupEnd", + None, + ) diff --git a/playwright/_impl/_waiter.py b/playwright/_impl/_waiter.py index 7b0ad2cc6..f7ff4b6c1 100644 --- a/playwright/_impl/_waiter.py +++ b/playwright/_impl/_waiter.py @@ -38,6 +38,7 @@ def __init__(self, channel_owner: ChannelOwner, event: str) -> None: def _wait_for_event_info_before(self, wait_id: str, event: str) -> None: self._channel.send_no_reply( "waitForEventInfo", + None, { "info": { "waitId": wait_id, @@ -51,6 +52,7 @@ def _wait_for_event_info_after(self, wait_id: str, error: Exception = None) -> N self._channel._connection.wrap_api_call_sync( lambda: self._channel.send_no_reply( "waitForEventInfo", + None, { "info": { "waitId": wait_id, @@ -130,6 +132,7 @@ def log(self, message: str) -> None: self._channel._connection.wrap_api_call_sync( lambda: self._channel.send_no_reply( "waitForEventInfo", + None, { "info": { "waitId": self._wait_id, diff --git a/playwright/_impl/_writable_stream.py b/playwright/_impl/_writable_stream.py index 702adf153..7d5b7704b 100644 --- a/playwright/_impl/_writable_stream.py +++ b/playwright/_impl/_writable_stream.py @@ -37,6 +37,6 @@ async def copy(self, path: Union[str, Path]) -> None: if not data: break await self._channel.send( - "write", {"binary": base64.b64encode(data).decode()} + "write", None, {"binary": base64.b64encode(data).decode()} ) - await self._channel.send("close") + await self._channel.send("close", None) diff --git a/playwright/async_api/__init__.py b/playwright/async_api/__init__.py index be918f53c..c05735fcd 100644 --- a/playwright/async_api/__init__.py +++ b/playwright/async_api/__init__.py @@ -30,7 +30,6 @@ from playwright._impl._assertions import PageAssertions as PageAssertionsImpl from playwright.async_api._context_manager import PlaywrightContextManager from playwright.async_api._generated import ( - Accessibility, APIRequest, APIRequestContext, APIResponse, @@ -79,6 +78,7 @@ ResourceTiming = playwright._impl._api_structures.ResourceTiming SourceLocation = playwright._impl._api_structures.SourceLocation StorageState = playwright._impl._api_structures.StorageState +StorageStateCookie = playwright._impl._api_structures.StorageStateCookie ViewportSize = playwright._impl._api_structures.ViewportSize Error = playwright._impl._errors.Error @@ -149,7 +149,6 @@ def __call__( __all__ = [ "expect", "async_playwright", - "Accessibility", "APIRequest", "APIRequestContext", "APIResponse", @@ -187,6 +186,7 @@ def __call__( "Selectors", "SourceLocation", "StorageState", + "StorageStateCookie", "TimeoutError", "Touchscreen", "Video", diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index d2f93dbb6..469046b21 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -18,7 +18,6 @@ import typing from typing import Literal -from playwright._impl._accessibility import Accessibility as AccessibilityImpl from playwright._impl._api_structures import ( ClientCertificate, Cookie, @@ -114,6 +113,24 @@ def resource_type(self) -> str: """ return mapping.from_maybe_impl(self._impl_obj.resource_type) + @property + def service_worker(self) -> typing.Optional["Worker"]: + """Request.service_worker + + The Service `Worker` that is performing the request. + + **Details** + + This method is Chromium only. It's safe to call when using other browsers, but it will always be `null`. + + Requests originated in a Service Worker do not have a `request.frame()` available. + + Returns + ------- + Union[Worker, None] + """ + return mapping.from_impl_nullable(self._impl_obj.service_worker) + @property def method(self) -> str: """Request.method @@ -674,7 +691,7 @@ async def fulfill( headers: typing.Optional[typing.Dict[str, str]] = None, body: typing.Optional[typing.Union[str, bytes]] = None, json: typing.Optional[typing.Any] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content_type: typing.Optional[str] = None, response: typing.Optional["APIResponse"] = None, ) -> None: @@ -929,6 +946,10 @@ async def handle(route, request): `route.continue_()` will immediately send the request to the network, other matching handlers won't be invoked. Use `route.fallback()` If you want next matching handler in the chain to be invoked. + **NOTE** The `Cookie` header cannot be overridden using this method. If a value is provided, it will be ignored, + and the cookie will be loaded from the browser's cookie store. To set custom cookies, use + `browser_context.add_cookies()`. + Parameters ---------- url : Union[str, None] @@ -1454,7 +1475,8 @@ async def move( y : float Y coordinate relative to the main frame's viewport in CSS pixels. steps : Union[int, None] - Defaults to 1. Sends intermediate `mousemove` events. + Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor + position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. """ return mapping.from_maybe_impl(await self._impl_obj.move(x=x, y=y, steps=steps)) @@ -2080,6 +2102,7 @@ async def click( force: typing.Optional[bool] = None, no_wait_after: typing.Optional[bool] = None, trial: typing.Optional[bool] = None, + steps: typing.Optional[int] = None, ) -> None: """ElementHandle.click @@ -2122,6 +2145,9 @@ async def click( trial : Union[bool, None] When set, this method only performs the [actionability](../actionability.md) checks and skips the action. Defaults to `false`. Useful to wait until the element is ready for the action without performing it. + steps : Union[int, None] + Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor + position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. """ return mapping.from_maybe_impl( @@ -2135,6 +2161,7 @@ async def click( force=force, noWaitAfter=no_wait_after, trial=trial, + steps=steps, ) ) @@ -2151,6 +2178,7 @@ async def dblclick( force: typing.Optional[bool] = None, no_wait_after: typing.Optional[bool] = None, trial: typing.Optional[bool] = None, + steps: typing.Optional[int] = None, ) -> None: """ElementHandle.dblclick @@ -2190,6 +2218,9 @@ async def dblclick( trial : Union[bool, None] When set, this method only performs the [actionability](../actionability.md) checks and skips the action. Defaults to `false`. Useful to wait until the element is ready for the action without performing it. + steps : Union[int, None] + Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor + position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. """ return mapping.from_maybe_impl( @@ -2202,6 +2233,7 @@ async def dblclick( force=force, noWaitAfter=no_wait_after, trial=trial, + steps=steps, ) ) @@ -2766,7 +2798,7 @@ async def screenshot( *, timeout: typing.Optional[float] = None, type: typing.Optional[Literal["jpeg", "png"]] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, quality: typing.Optional[int] = None, omit_background: typing.Optional[bool] = None, animations: typing.Optional[Literal["allow", "disabled"]] = None, @@ -3086,72 +3118,6 @@ async def wait_for_selector( mapping.register(ElementHandleImpl, ElementHandle) -class Accessibility(AsyncBase): - - async def snapshot( - self, - *, - interesting_only: typing.Optional[bool] = None, - root: typing.Optional["ElementHandle"] = None, - ) -> typing.Optional[typing.Dict]: - """Accessibility.snapshot - - Captures the current state of the accessibility tree. The returned object represents the root accessible node of - the page. - - **NOTE** The Chromium accessibility tree contains nodes that go unused on most platforms and by most screen - readers. Playwright will discard them as well for an easier to process tree, unless `interestingOnly` is set to - `false`. - - **Usage** - - An example of dumping the entire accessibility tree: - - ```py - snapshot = await page.accessibility.snapshot() - print(snapshot) - ``` - - An example of logging the focused node's name: - - ```py - def find_focused_node(node): - if node.get(\"focused\"): - return node - for child in (node.get(\"children\") or []): - found_node = find_focused_node(child) - if found_node: - return found_node - return None - - snapshot = await page.accessibility.snapshot() - node = find_focused_node(snapshot) - if node: - print(node[\"name\"]) - ``` - - Parameters - ---------- - interesting_only : Union[bool, None] - Prune uninteresting nodes from the tree. Defaults to `true`. - root : Union[ElementHandle, None] - The root DOM element for the snapshot. Defaults to the whole page. - - Returns - ------- - Union[Dict, None] - """ - - return mapping.from_maybe_impl( - await self._impl_obj.snapshot( - interestingOnly=interesting_only, root=mapping.to_impl(root) - ) - ) - - -mapping.register(AccessibilityImpl, Accessibility) - - class FileChooser(AsyncBase): @property @@ -3914,11 +3880,7 @@ async def is_enabled( ) async def is_hidden( - self, - selector: str, - *, - strict: typing.Optional[bool] = None, - timeout: typing.Optional[float] = None, + self, selector: str, *, strict: typing.Optional[bool] = None ) -> bool: """Frame.is_hidden @@ -3933,8 +3895,6 @@ async def is_hidden( strict : Union[bool, None] When true, the call requires selector to resolve to a single element. If given selector resolves to more than one element, the call throws an exception. - timeout : Union[float, None] - Deprecated: This option is ignored. `frame.is_hidden()` does not wait for the element to become hidden and returns immediately. Returns ------- @@ -3942,17 +3902,11 @@ async def is_hidden( """ return mapping.from_maybe_impl( - await self._impl_obj.is_hidden( - selector=selector, strict=strict, timeout=timeout - ) + await self._impl_obj.is_hidden(selector=selector, strict=strict) ) async def is_visible( - self, - selector: str, - *, - strict: typing.Optional[bool] = None, - timeout: typing.Optional[float] = None, + self, selector: str, *, strict: typing.Optional[bool] = None ) -> bool: """Frame.is_visible @@ -3967,8 +3921,6 @@ async def is_visible( strict : Union[bool, None] When true, the call requires selector to resolve to a single element. If given selector resolves to more than one element, the call throws an exception. - timeout : Union[float, None] - Deprecated: This option is ignored. `frame.is_visible()` does not wait for the element to become visible and returns immediately. Returns ------- @@ -3976,9 +3928,7 @@ async def is_visible( """ return mapping.from_maybe_impl( - await self._impl_obj.is_visible( - selector=selector, strict=strict, timeout=timeout - ) + await self._impl_obj.is_visible(selector=selector, strict=strict) ) async def dispatch_event( @@ -4212,7 +4162,7 @@ async def add_script_tag( self, *, url: typing.Optional[str] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content: typing.Optional[str] = None, type: typing.Optional[str] = None, ) -> "ElementHandle": @@ -4250,7 +4200,7 @@ async def add_style_tag( self, *, url: typing.Optional[str] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content: typing.Optional[str] = None, ) -> "ElementHandle": """Frame.add_style_tag @@ -4572,8 +4522,8 @@ def locator( self, selector: str, *, - has_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - has_not_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + has_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + has_not_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, has: typing.Optional["Locator"] = None, has_not: typing.Optional["Locator"] = None, ) -> "Locator": @@ -4840,7 +4790,7 @@ def get_by_role( expanded: typing.Optional[bool] = None, include_hidden: typing.Optional[bool] = None, level: typing.Optional[int] = None, - name: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + name: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, pressed: typing.Optional[bool] = None, selected: typing.Optional[bool] = None, exact: typing.Optional[bool] = None, @@ -5369,6 +5319,7 @@ async def drag_and_drop( strict: typing.Optional[bool] = None, timeout: typing.Optional[float] = None, trial: typing.Optional[bool] = None, + steps: typing.Optional[int] = None, ) -> None: """Frame.drag_and_drop @@ -5400,6 +5351,9 @@ async def drag_and_drop( trial : Union[bool, None] When set, this method only performs the [actionability](../actionability.md) checks and skips the action. Defaults to `false`. Useful to wait until the element is ready for the action without performing it. + steps : Union[int, None] + Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between the `mousedown` and `mouseup` + of the drag. When set to 1, emits a single `mousemove` event at the destination location. """ return mapping.from_maybe_impl( @@ -5413,6 +5367,7 @@ async def drag_and_drop( strict=strict, timeout=timeout, trial=trial, + steps=steps, ) ) @@ -6057,8 +6012,8 @@ def locator( self, selector_or_locator: typing.Union["Locator", str], *, - has_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - has_not_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + has_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + has_not_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, has: typing.Optional["Locator"] = None, has_not: typing.Optional["Locator"] = None, ) -> "Locator": @@ -6322,7 +6277,7 @@ def get_by_role( expanded: typing.Optional[bool] = None, include_hidden: typing.Optional[bool] = None, level: typing.Optional[int] = None, - name: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + name: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, pressed: typing.Optional[bool] = None, selected: typing.Optional[bool] = None, exact: typing.Optional[bool] = None, @@ -6612,6 +6567,7 @@ def nth(self, index: int) -> "FrameLocator": class Worker(AsyncBase): + @typing.overload def on( self, event: Literal["close"], @@ -6620,8 +6576,27 @@ def on( """ Emitted when this dedicated [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is terminated.""" + + @typing.overload + def on( + self, + event: Literal["console"], + f: typing.Callable[ + ["ConsoleMessage"], "typing.Union[typing.Awaitable[None], None]" + ], + ) -> None: + """ + Emitted when JavaScript within the worker calls one of console API methods, e.g. `console.log` or `console.dir`. + """ + + def on( + self, + event: str, + f: typing.Callable[..., typing.Union[typing.Awaitable[None], None]], + ) -> None: return super().on(event=event, f=f) + @typing.overload def once( self, event: Literal["close"], @@ -6630,6 +6605,24 @@ def once( """ Emitted when this dedicated [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is terminated.""" + + @typing.overload + def once( + self, + event: Literal["console"], + f: typing.Callable[ + ["ConsoleMessage"], "typing.Union[typing.Awaitable[None], None]" + ], + ) -> None: + """ + Emitted when JavaScript within the worker calls one of console API methods, e.g. `console.log` or `console.dir`. + """ + + def once( + self, + event: str, + f: typing.Callable[..., typing.Union[typing.Awaitable[None], None]], + ) -> None: return super().once(event=event, f=f) @property @@ -6707,6 +6700,47 @@ async def evaluate_handle( ) ) + def expect_event( + self, + event: str, + predicate: typing.Optional[typing.Callable] = None, + *, + timeout: typing.Optional[float] = None, + ) -> AsyncEventContextManager: + """Worker.expect_event + + Waits for event to fire and passes its value into the predicate function. Returns when the predicate returns truthy + value. Will throw an error if the page is closed before the event is fired. Returns the event data value. + + **Usage** + + ```py + async with worker.expect_event(\"console\") as event_info: + await worker.evaluate(\"console.log(42)\") + message = await event_info.value + ``` + + Parameters + ---------- + event : str + Event name, same one typically passed into `*.on(event)`. + predicate : Union[Callable, None] + Receives the event data and resolves to truthy value when the waiting should resolve. + timeout : Union[float, None] + Maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The + default value can be changed by using the `browser_context.set_default_timeout()`. + + Returns + ------- + EventContextManager + """ + + return AsyncEventContextManager( + self._impl_obj.expect_event( + event=event, predicate=self._wrap_handler(predicate), timeout=timeout + ).future + ) + mapping.register(WorkerImpl, Worker) @@ -6718,7 +6752,7 @@ async def register( name: str, script: typing.Optional[str] = None, *, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content_script: typing.Optional[bool] = None, ) -> None: """Selectors.register @@ -6987,16 +7021,33 @@ async def set_system_time( class ConsoleMessage(AsyncBase): @property - def type(self) -> str: + def type( + self, + ) -> typing.Union[ + Literal["assert"], + Literal["clear"], + Literal["count"], + Literal["debug"], + Literal["dir"], + Literal["dirxml"], + Literal["endGroup"], + Literal["error"], + Literal["info"], + Literal["log"], + Literal["profile"], + Literal["profileEnd"], + Literal["startGroup"], + Literal["startGroupCollapsed"], + Literal["table"], + Literal["timeEnd"], + Literal["trace"], + Literal["warning"], + ]: """ConsoleMessage.type - One of the following values: `'log'`, `'debug'`, `'info'`, `'error'`, `'warning'`, `'dir'`, `'dirxml'`, `'table'`, - `'trace'`, `'clear'`, `'startGroup'`, `'startGroupCollapsed'`, `'endGroup'`, `'assert'`, `'profile'`, - `'profileEnd'`, `'count'`, `'timeEnd'`. - Returns ------- - str + Union["assert", "clear", "count", "debug", "dir", "dirxml", "endGroup", "error", "info", "log", "profile", "profileEnd", "startGroup", "startGroupCollapsed", "table", "timeEnd", "trace", "warning"] """ return mapping.from_maybe_impl(self._impl_obj.type) @@ -7046,6 +7097,19 @@ def page(self) -> typing.Optional["Page"]: """ return mapping.from_impl_nullable(self._impl_obj.page) + @property + def worker(self) -> typing.Optional["Worker"]: + """ConsoleMessage.worker + + The web worker or service worker that produced this console message, if any. Note that console messages from web + workers also have non-null `console_message.page()`. + + Returns + ------- + Union[Worker, None] + """ + return mapping.from_impl_nullable(self._impl_obj.worker) + mapping.register(ConsoleMessageImpl, ConsoleMessage) @@ -7820,16 +7884,6 @@ def once( ) -> None: return super().once(event=event, f=f) - @property - def accessibility(self) -> "Accessibility": - """Page.accessibility - - Returns - ------- - Accessibility - """ - return mapping.from_impl(self._impl_obj.accessibility) - @property def keyboard(self) -> "Keyboard": """Page.keyboard @@ -8658,7 +8712,7 @@ async def add_script_tag( self, *, url: typing.Optional[str] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content: typing.Optional[str] = None, type: typing.Optional[str] = None, ) -> "ElementHandle": @@ -8695,7 +8749,7 @@ async def add_style_tag( self, *, url: typing.Optional[str] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content: typing.Optional[str] = None, ) -> "ElementHandle": """Page.add_style_tag @@ -9381,7 +9435,7 @@ async def add_init_script( self, script: typing.Optional[str] = None, *, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, ) -> None: """Page.add_init_script @@ -9486,8 +9540,8 @@ async def handle_route(route: Route): Parameters ---------- url : Union[Callable[[str], bool], Pattern[str], str] - A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a `baseURL` via the context - options was provided and the passed URL is a path, it gets merged via the + A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If `baseURL` is set in + the context options and the provided URL is a string that does not start with `*`, it is resolved using the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. handler : Union[Callable[[Route, Request], Any], Callable[[Route], Any]] handler function to route the request. @@ -9603,7 +9657,7 @@ async def route_from_har( self, har: typing.Union[pathlib.Path, str], *, - url: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + url: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, not_found: typing.Optional[Literal["abort", "fallback"]] = None, update: typing.Optional[bool] = None, update_content: typing.Optional[Literal["attach", "embed"]] = None, @@ -9659,7 +9713,7 @@ async def screenshot( *, timeout: typing.Optional[float] = None, type: typing.Optional[Literal["jpeg", "png"]] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, quality: typing.Optional[int] = None, omit_background: typing.Optional[bool] = None, full_page: typing.Optional[bool] = None, @@ -10093,8 +10147,8 @@ def locator( self, selector: str, *, - has_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - has_not_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + has_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + has_not_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, has: typing.Optional["Locator"] = None, has_not: typing.Optional["Locator"] = None, ) -> "Locator": @@ -10359,7 +10413,7 @@ def get_by_role( expanded: typing.Optional[bool] = None, include_hidden: typing.Optional[bool] = None, level: typing.Optional[int] = None, - name: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + name: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, pressed: typing.Optional[bool] = None, selected: typing.Optional[bool] = None, exact: typing.Optional[bool] = None, @@ -10888,6 +10942,7 @@ async def drag_and_drop( timeout: typing.Optional[float] = None, strict: typing.Optional[bool] = None, trial: typing.Optional[bool] = None, + steps: typing.Optional[int] = None, ) -> None: """Page.drag_and_drop @@ -10935,6 +10990,9 @@ async def drag_and_drop( trial : Union[bool, None] When set, this method only performs the [actionability](../actionability.md) checks and skips the action. Defaults to `false`. Useful to wait until the element is ready for the action without performing it. + steps : Union[int, None] + Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between the `mousedown` and `mouseup` + of the drag. When set to 1, emits a single `mousemove` event at the destination location. """ return mapping.from_maybe_impl( @@ -10948,6 +11006,7 @@ async def drag_and_drop( timeout=timeout, strict=strict, trial=trial, + steps=steps, ) ) @@ -11482,8 +11541,8 @@ async def main(): async def pause(self) -> None: """Page.pause - Pauses script execution. Playwright will stop executing the script and wait for the user to either press 'Resume' - button in the page overlay or to call `playwright.resume()` in the DevTools console. + Pauses script execution. Playwright will stop executing the script and wait for the user to either press the + 'Resume' button in the page overlay or to call `playwright.resume()` in the DevTools console. User can inspect selectors or perform manual steps while paused. Resume will continue running the original script from the place it was paused. @@ -11508,7 +11567,7 @@ async def pdf( height: typing.Optional[typing.Union[str, float]] = None, prefer_css_page_size: typing.Optional[bool] = None, margin: typing.Optional[PdfMargins] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, outline: typing.Optional[bool] = None, tagged: typing.Optional[bool] = None, ) -> bytes: @@ -12249,6 +12308,49 @@ async def remove_locator_handler(self, locator: "Locator") -> None: await self._impl_obj.remove_locator_handler(locator=locator._impl_obj) ) + async def requests(self) -> typing.List["Request"]: + """Page.requests + + Returns up to (currently) 100 last network request from this page. See `page.on('request')` for more details. + + Returned requests should be accessed immediately, otherwise they might be collected to prevent unbounded memory + growth as new requests come in. Once collected, retrieving most information about the request is impossible. + + Note that requests reported through the `page.on('request')` request are not collected, so there is a trade off + between efficient memory usage with `page.requests()` and the amount of available information reported + through `page.on('request')`. + + Returns + ------- + List[Request] + """ + + return mapping.from_impl_list(await self._impl_obj.requests()) + + async def console_messages(self) -> typing.List["ConsoleMessage"]: + """Page.console_messages + + Returns up to (currently) 200 last console messages from this page. See `page.on('console')` for more details. + + Returns + ------- + List[ConsoleMessage] + """ + + return mapping.from_impl_list(await self._impl_obj.console_messages()) + + async def page_errors(self) -> typing.List["Error"]: + """Page.page_errors + + Returns up to (currently) 200 last page errors from this page. See `page.on('page_error')` for more details. + + Returns + ------- + List[Error] + """ + + return mapping.from_impl_list(await self._impl_obj.page_errors()) + mapping.register(PageImpl, Page) @@ -12292,13 +12394,7 @@ def on( f: typing.Callable[["Page"], "typing.Union[typing.Awaitable[None], None]"], ) -> None: """ - **NOTE** Only works with Chromium browser's persistent context. - - Emitted when new background page is created in the context. - - ```py - background_page = await context.wait_for_event(\"backgroundpage\") - ```""" + This event is not emitted.""" @typing.overload def on( @@ -12472,13 +12568,7 @@ def once( f: typing.Callable[["Page"], "typing.Union[typing.Awaitable[None], None]"], ) -> None: """ - **NOTE** Only works with Chromium browser's persistent context. - - Emitted when new background page is created in the context. - - ```py - background_page = await context.wait_for_event(\"backgroundpage\") - ```""" + This event is not emitted.""" @typing.overload def once( @@ -12661,7 +12751,8 @@ def pages(self) -> typing.List["Page"]: def browser(self) -> typing.Optional["Browser"]: """BrowserContext.browser - Returns the browser instance of the context. If it was launched as a persistent context null gets returned. + Gets the browser instance that owns the context. Returns `null` if the context is created outside of normal + browser, e.g. Android or Electron. Returns ------- @@ -12673,9 +12764,7 @@ def browser(self) -> typing.Optional["Browser"]: def background_pages(self) -> typing.List["Page"]: """BrowserContext.background_pages - **NOTE** Background pages are only supported on Chromium-based browsers. - - All existing background pages in the context. + Returns an empty list. Returns ------- @@ -12801,7 +12890,7 @@ async def cookies( Returns ------- - List[{name: str, value: str, domain: str, path: str, expires: float, httpOnly: bool, secure: bool, sameSite: Union["Lax", "None", "Strict"]}] + List[{name: str, value: str, domain: str, path: str, expires: float, httpOnly: bool, secure: bool, sameSite: Union["Lax", "None", "Strict"], partitionKey: Union[str, None]}] """ return mapping.from_impl_list( @@ -12822,7 +12911,7 @@ async def add_cookies(self, cookies: typing.Sequence[SetCookieParam]) -> None: Parameters ---------- - cookies : Sequence[{name: str, value: str, url: Union[str, None], domain: Union[str, None], path: Union[str, None], expires: Union[float, None], httpOnly: Union[bool, None], secure: Union[bool, None], sameSite: Union["Lax", "None", "Strict", None]}] + cookies : Sequence[{name: str, value: str, url: Union[str, None], domain: Union[str, None], path: Union[str, None], expires: Union[float, None], httpOnly: Union[bool, None], secure: Union[bool, None], sameSite: Union["Lax", "None", "Strict", None], partitionKey: Union[str, None]}] """ return mapping.from_maybe_impl( @@ -12832,9 +12921,9 @@ async def add_cookies(self, cookies: typing.Sequence[SetCookieParam]) -> None: async def clear_cookies( self, *, - name: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - domain: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - path: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + name: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + domain: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + path: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, ) -> None: """BrowserContext.clear_cookies @@ -12889,6 +12978,8 @@ async def grant_permissions( - `'clipboard-write'` - `'geolocation'` - `'gyroscope'` + - `'local-fonts'` + - `'local-network-access'` - `'magnetometer'` - `'microphone'` - `'midi-sysex'` (system-exclusive midi) @@ -12987,7 +13078,7 @@ async def add_init_script( self, script: typing.Optional[str] = None, *, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, ) -> None: """BrowserContext.add_init_script @@ -13216,8 +13307,8 @@ async def handle_route(route: Route): Parameters ---------- url : Union[Callable[[str], bool], Pattern[str], str] - A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a `baseURL` via the context - options was provided and the passed URL is a path, it gets merged via the + A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If `baseURL` is set in + the context options and the provided URL is a string that does not start with `*`, it is resolved using the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. handler : Union[Callable[[Route, Request], Any], Callable[[Route], Any]] handler function to route the request. @@ -13336,7 +13427,7 @@ async def route_from_har( self, har: typing.Union[pathlib.Path, str], *, - url: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + url: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, not_found: typing.Optional[Literal["abort", "fallback"]] = None, update: typing.Optional[bool] = None, update_content: typing.Optional[Literal["attach", "embed"]] = None, @@ -13446,7 +13537,7 @@ async def close(self, *, reason: typing.Optional[str] = None) -> None: async def storage_state( self, *, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, indexed_db: typing.Optional[bool] = None, ) -> StorageState: """BrowserContext.storage_state @@ -13464,9 +13555,6 @@ async def storage_state( state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase Authentication, enable this. - **NOTE** IndexedDBs with typed arrays are currently not supported. - - Returns ------- {cookies: List[{name: str, value: str, domain: str, path: str, expires: float, httpOnly: bool, secure: bool, sameSite: Union["Lax", "None", "Strict"]}], origins: List[{origin: str, localStorage: List[{name: str, value: str}]}]} @@ -13751,9 +13839,9 @@ async def new_context( accept_downloads: typing.Optional[bool] = None, default_browser_type: typing.Optional[str] = None, proxy: typing.Optional[ProxySettings] = None, - record_har_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_har_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_har_omit_content: typing.Optional[bool] = None, - record_video_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_video_dir: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_video_size: typing.Optional[ViewportSize] = None, storage_state: typing.Optional[ typing.Union[StorageState, str, pathlib.Path] @@ -13762,7 +13850,7 @@ async def new_context( strict_selectors: typing.Optional[bool] = None, service_workers: typing.Optional[Literal["allow", "block"]] = None, record_har_url_filter: typing.Optional[ - typing.Union[str, typing.Pattern[str]] + typing.Union[typing.Pattern[str], str] ] = None, record_har_mode: typing.Optional[Literal["full", "minimal"]] = None, record_har_content: typing.Optional[Literal["attach", "embed", "omit"]] = None, @@ -13917,6 +14005,10 @@ async def new_context( `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided with an exact match to the request origin that the certificate is valid for. + Client certificate authentication is only active when at least one client certificate is provided. If you want to + reject all client certificates sent by the server, you need to provide a client certificate with an `origin` that + does not match any of the domains you plan to visit. + **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it work by replacing `localhost` with `local.playwright`. @@ -13998,9 +14090,9 @@ async def new_page( accept_downloads: typing.Optional[bool] = None, default_browser_type: typing.Optional[str] = None, proxy: typing.Optional[ProxySettings] = None, - record_har_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_har_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_har_omit_content: typing.Optional[bool] = None, - record_video_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_video_dir: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_video_size: typing.Optional[ViewportSize] = None, storage_state: typing.Optional[ typing.Union[StorageState, str, pathlib.Path] @@ -14009,7 +14101,7 @@ async def new_page( strict_selectors: typing.Optional[bool] = None, service_workers: typing.Optional[Literal["allow", "block"]] = None, record_har_url_filter: typing.Optional[ - typing.Union[str, typing.Pattern[str]] + typing.Union[typing.Pattern[str], str] ] = None, record_har_mode: typing.Optional[Literal["full", "minimal"]] = None, record_har_content: typing.Optional[Literal["attach", "embed", "omit"]] = None, @@ -14148,6 +14240,10 @@ async def new_page( `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided with an exact match to the request origin that the certificate is valid for. + Client certificate authentication is only active when at least one client certificate is provided. If you want to + reject all client certificates sent by the server, you need to provide a client certificate with an `origin` that + does not match any of the domains you plan to visit. + **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it work by replacing `localhost` with `local.playwright`. @@ -14239,7 +14335,7 @@ async def start_tracing( self, *, page: typing.Optional["Page"] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, screenshots: typing.Optional[bool] = None, categories: typing.Optional[typing.Sequence[str]] = None, ) -> None: @@ -14332,7 +14428,7 @@ def executable_path(self) -> str: async def launch( self, *, - executable_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + executable_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, channel: typing.Optional[str] = None, args: typing.Optional[typing.Sequence[str]] = None, ignore_default_args: typing.Optional[ @@ -14346,9 +14442,9 @@ async def launch( headless: typing.Optional[bool] = None, devtools: typing.Optional[bool] = None, proxy: typing.Optional[ProxySettings] = None, - downloads_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + downloads_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, slow_mo: typing.Optional[float] = None, - traces_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, + traces_dir: typing.Optional[typing.Union[pathlib.Path, str]] = None, chromium_sandbox: typing.Optional[bool] = None, firefox_user_prefs: typing.Optional[ typing.Dict[str, typing.Union[str, float, bool]] @@ -14418,7 +14514,7 @@ async def launch( headless : Union[bool, None] Whether to run browser in headless mode. More details for [Chromium](https://developers.google.com/web/updates/2017/04/headless-chrome) and - [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode). Defaults to `true` unless the + [Firefox](https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/). Defaults to `true` unless the `devtools` option is `true`. devtools : Union[bool, None] **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the @@ -14441,6 +14537,9 @@ async def launch( Firefox user preferences. Learn more about the Firefox user preferences at [`about:config`](https://support.mozilla.org/en-US/kb/about-config-editor-firefox). + You can also provide a path to a custom [`policies.json` file](https://mozilla.github.io/policy-templates/) via + `PLAYWRIGHT_FIREFOX_POLICIES_JSON` environment variable. + Returns ------- Browser @@ -14473,7 +14572,7 @@ async def launch_persistent_context( user_data_dir: typing.Union[str, pathlib.Path], *, channel: typing.Optional[str] = None, - executable_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + executable_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, args: typing.Optional[typing.Sequence[str]] = None, ignore_default_args: typing.Optional[ typing.Union[bool, typing.Sequence[str]] @@ -14486,7 +14585,7 @@ async def launch_persistent_context( headless: typing.Optional[bool] = None, devtools: typing.Optional[bool] = None, proxy: typing.Optional[ProxySettings] = None, - downloads_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + downloads_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, slow_mo: typing.Optional[float] = None, viewport: typing.Optional[ViewportSize] = None, screen: typing.Optional[ViewportSize] = None, @@ -14514,20 +14613,20 @@ async def launch_persistent_context( forced_colors: typing.Optional[Literal["active", "none", "null"]] = None, contrast: typing.Optional[Literal["more", "no-preference", "null"]] = None, accept_downloads: typing.Optional[bool] = None, - traces_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, + traces_dir: typing.Optional[typing.Union[pathlib.Path, str]] = None, chromium_sandbox: typing.Optional[bool] = None, firefox_user_prefs: typing.Optional[ typing.Dict[str, typing.Union[str, float, bool]] ] = None, - record_har_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_har_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_har_omit_content: typing.Optional[bool] = None, - record_video_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_video_dir: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_video_size: typing.Optional[ViewportSize] = None, base_url: typing.Optional[str] = None, strict_selectors: typing.Optional[bool] = None, service_workers: typing.Optional[Literal["allow", "block"]] = None, record_har_url_filter: typing.Optional[ - typing.Union[str, typing.Pattern[str]] + typing.Union[typing.Pattern[str], str] ] = None, record_har_mode: typing.Optional[Literal["full", "minimal"]] = None, record_har_content: typing.Optional[Literal["attach", "embed", "omit"]] = None, @@ -14543,11 +14642,22 @@ async def launch_persistent_context( Parameters ---------- user_data_dir : Union[pathlib.Path, str] - Path to a User Data Directory, which stores browser session data like cookies and local storage. More details for + Path to a User Data Directory, which stores browser session data like cookies and local storage. Pass an empty + string to create a temporary directory. + + More details for [Chromium](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md#introduction) and - [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#User_Profile). Note that Chromium's - user data directory is the **parent** directory of the "Profile Path" seen at `chrome://version`. Pass an empty - string to use a temporary directory instead. + [Firefox](https://wiki.mozilla.org/Firefox/CommandLineOptions#User_profile). Chromium's user data directory is the + **parent** directory of the "Profile Path" seen at `chrome://version`. + + Note that browsers do not allow launching multiple instances with the same User Data Directory. + + **NOTE** Chromium/Chrome: Due to recent Chrome policy changes, automating the default Chrome user profile is not + supported. Pointing `userDataDir` to Chrome's main "User Data" directory (the profile used for your regular + browsing) may result in pages not loading or the browser exiting. Create and use a separate directory (for example, + an empty folder) as your automation profile instead. See https://developer.chrome.com/blog/remote-debugging-port + for details. + channel : Union[str, None] Browser distribution channel. @@ -14581,7 +14691,7 @@ async def launch_persistent_context( headless : Union[bool, None] Whether to run browser in headless mode. More details for [Chromium](https://developers.google.com/web/updates/2017/04/headless-chrome) and - [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode). Defaults to `true` unless the + [Firefox](https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/). Defaults to `true` unless the `devtools` option is `true`. devtools : Union[bool, None] **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the @@ -14668,6 +14778,9 @@ async def launch_persistent_context( firefox_user_prefs : Union[Dict[str, Union[bool, float, str]], None] Firefox user preferences. Learn more about the Firefox user preferences at [`about:config`](https://support.mozilla.org/en-US/kb/about-config-editor-firefox). + + You can also provide a path to a custom [`policies.json` file](https://mozilla.github.io/policy-templates/) via + `PLAYWRIGHT_FIREFOX_POLICIES_JSON` environment variable. record_har_path : Union[pathlib.Path, str, None] Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into the specified HAR file on the filesystem. If not specified, the HAR is not recorded. Make sure to call `browser_context.close()` @@ -14719,6 +14832,10 @@ async def launch_persistent_context( `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided with an exact match to the request origin that the certificate is valid for. + Client certificate authentication is only active when at least one client certificate is provided. If you want to + reject all client certificates sent by the server, you need to provide a client certificate with an `origin` that + does not match any of the domains you plan to visit. + **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it work by replacing `localhost` with `local.playwright`. @@ -15042,6 +15159,15 @@ async def start( Start tracing. + **NOTE** You probably want to + [enable tracing in your config file](https://playwright.dev/docs/api/class-testoptions#test-options-trace) instead + of using `Tracing.start`. + + The `context.tracing` API captures browser operations and network activity, but it doesn't record test assertions + (like `expect` calls). We recommend + [enabling tracing through Playwright Test configuration](https://playwright.dev/docs/api/class-testoptions#test-options-trace), + which includes those assertions and provides a more complete trace for debugging test failures. + **Usage** ```py @@ -15121,7 +15247,7 @@ async def start_chunk( ) async def stop_chunk( - self, *, path: typing.Optional[typing.Union[str, pathlib.Path]] = None + self, *, path: typing.Optional[typing.Union[pathlib.Path, str]] = None ) -> None: """Tracing.stop_chunk @@ -15136,7 +15262,7 @@ async def stop_chunk( return mapping.from_maybe_impl(await self._impl_obj.stop_chunk(path=path)) async def stop( - self, *, path: typing.Optional[typing.Union[str, pathlib.Path]] = None + self, *, path: typing.Optional[typing.Union[pathlib.Path, str]] = None ) -> None: """Tracing.stop @@ -15266,6 +15392,30 @@ def content_frame(self) -> "FrameLocator": """ return mapping.from_impl(self._impl_obj.content_frame) + @property + def description(self) -> typing.Optional[str]: + """Locator.description + + Returns locator description previously set with `locator.describe()`. Returns `null` if no custom + description has been set. Prefer `Locator.toString()` for a human-readable representation, as it uses the + description when available. + + **Usage** + + ```py + button = page.get_by_role(\"button\").describe(\"Subscribe button\") + print(button.description()) # \"Subscribe button\" + + input = page.get_by_role(\"textbox\") + print(input.description()) # None + ``` + + Returns + ------- + Union[str, None] + """ + return mapping.from_maybe_impl(self._impl_obj.description) + async def bounding_box( self, *, timeout: typing.Optional[float] = None ) -> typing.Optional[FloatRect]: @@ -15385,6 +15535,7 @@ async def click( force: typing.Optional[bool] = None, no_wait_after: typing.Optional[bool] = None, trial: typing.Optional[bool] = None, + steps: typing.Optional[int] = None, ) -> None: """Locator.click @@ -15449,6 +15600,9 @@ async def click( to `false`. Useful to wait until the element is ready for the action without performing it. Note that keyboard `modifiers` will be pressed regardless of `trial` to allow testing elements which are only visible when those keys are pressed. + steps : Union[int, None] + Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor + position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. """ return mapping.from_maybe_impl( @@ -15462,6 +15616,7 @@ async def click( force=force, noWaitAfter=no_wait_after, trial=trial, + steps=steps, ) ) @@ -15478,6 +15633,7 @@ async def dblclick( force: typing.Optional[bool] = None, no_wait_after: typing.Optional[bool] = None, trial: typing.Optional[bool] = None, + steps: typing.Optional[int] = None, ) -> None: """Locator.dblclick @@ -15523,6 +15679,9 @@ async def dblclick( to `false`. Useful to wait until the element is ready for the action without performing it. Note that keyboard `modifiers` will be pressed regardless of `trial` to allow testing elements which are only visible when those keys are pressed. + steps : Union[int, None] + Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor + position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. """ return mapping.from_maybe_impl( @@ -15535,6 +15694,7 @@ async def dblclick( force=force, noWaitAfter=no_wait_after, trial=trial, + steps=steps, ) ) @@ -15622,6 +15782,13 @@ async def evaluate( **Usage** + Passing argument to `expression`: + + ```py + result = await page.get_by_testid(\"myId\").evaluate(\"(element, [x, y]) => element.textContent + ' ' + x * y\", [7, 8]) + print(result) # prints \"myId text 56\" + ``` + Parameters ---------- expression : str @@ -15630,8 +15797,8 @@ async def evaluate( arg : Union[Any, None] Optional argument to pass to `expression`. timeout : Union[float, None] - Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can - be changed by using the `browser_context.set_default_timeout()` or `page.set_default_timeout()` methods. + Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, + evaluation itself is not limited by the timeout. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. Returns ------- @@ -15720,8 +15887,8 @@ async def evaluate_handle( arg : Union[Any, None] Optional argument to pass to `expression`. timeout : Union[float, None] - Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can - be changed by using the `browser_context.set_default_timeout()` or `page.set_default_timeout()` methods. + Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, + evaluation itself is not limited by the timeout. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. Returns ------- @@ -15833,8 +16000,8 @@ def locator( self, selector_or_locator: typing.Union[str, "Locator"], *, - has_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - has_not_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + has_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + has_not_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, has: typing.Optional["Locator"] = None, has_not: typing.Optional["Locator"] = None, ) -> "Locator": @@ -16098,7 +16265,7 @@ def get_by_role( expanded: typing.Optional[bool] = None, include_hidden: typing.Optional[bool] = None, level: typing.Optional[int] = None, - name: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + name: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, pressed: typing.Optional[bool] = None, selected: typing.Optional[bool] = None, exact: typing.Optional[bool] = None, @@ -16428,11 +16595,36 @@ def nth(self, index: int) -> "Locator": return mapping.from_impl(self._impl_obj.nth(index=index)) + def describe(self, description: str) -> "Locator": + """Locator.describe + + Describes the locator, description is used in the trace viewer and reports. Returns the locator pointing to the + same element. + + **Usage** + + ```py + button = page.get_by_test_id(\"btn-sub\").describe(\"Subscribe button\") + await button.click() + ``` + + Parameters + ---------- + description : str + Locator description. + + Returns + ------- + Locator + """ + + return mapping.from_impl(self._impl_obj.describe(description=description)) + def filter( self, *, - has_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - has_not_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + has_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + has_not_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, has: typing.Optional["Locator"] = None, has_not: typing.Optional["Locator"] = None, visible: typing.Optional[bool] = None, @@ -16543,7 +16735,7 @@ def and_(self, locator: "Locator") -> "Locator": The following example finds a button with a specific title. ```py - button = page.get_by_role(\"button\").and_(page.getByTitle(\"Subscribe\")) + button = page.get_by_role(\"button\").and_(page.get_by_title(\"Subscribe\")) ``` Parameters @@ -16646,6 +16838,7 @@ async def drag_to( trial: typing.Optional[bool] = None, source_position: typing.Optional[Position] = None, target_position: typing.Optional[Position] = None, + steps: typing.Optional[int] = None, ) -> None: """Locator.drag_to @@ -16692,6 +16885,9 @@ async def drag_to( target_position : Union[{x: float, y: float}, None] Drops on the target element at this point relative to the top-left corner of the element's padding box. If not specified, some visible point of the element is used. + steps : Union[int, None] + Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between the `mousedown` and `mouseup` + of the drag. When set to 1, emits a single `mousemove` event at the destination location. """ return mapping.from_maybe_impl( @@ -16703,6 +16899,7 @@ async def drag_to( trial=trial, sourcePosition=source_position, targetPosition=target_position, + steps=steps, ) ) @@ -17112,7 +17309,7 @@ async def screenshot( *, timeout: typing.Optional[float] = None, type: typing.Optional[Literal["jpeg", "png"]] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, quality: typing.Optional[int] = None, omit_background: typing.Optional[bool] = None, animations: typing.Optional[Literal["allow", "disabled"]] = None, @@ -18654,7 +18851,7 @@ async def fetch( async def storage_state( self, *, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, indexed_db: typing.Optional[bool] = None, ) -> StorageState: """APIRequestContext.storage_state @@ -18700,6 +18897,7 @@ async def new_context( ] = None, client_certificates: typing.Optional[typing.List[ClientCertificate]] = None, fail_on_status_code: typing.Optional[bool] = None, + max_redirects: typing.Optional[int] = None, ) -> "APIRequestContext": """APIRequest.new_context @@ -18745,12 +18943,20 @@ async def new_context( `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided with an exact match to the request origin that the certificate is valid for. + Client certificate authentication is only active when at least one client certificate is provided. If you want to + reject all client certificates sent by the server, you need to provide a client certificate with an `origin` that + does not match any of the domains you plan to visit. + **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it work by replacing `localhost` with `local.playwright`. fail_on_status_code : Union[bool, None] Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes. + max_redirects : Union[int, None] + Maximum number of request redirects that will be followed automatically. An error will be thrown if the number is + exceeded. Defaults to `20`. Pass `0` to not follow redirects. This can be overwritten for each request + individually. Returns ------- @@ -18769,6 +18975,7 @@ async def new_context( storageState=storage_state, clientCertificates=client_certificates, failOnStatusCode=fail_on_status_code, + maxRedirects=max_redirects, ) ) @@ -19133,7 +19340,7 @@ async def to_have_class( """LocatorAssertions.to_have_class Ensures the `Locator` points to an element with given CSS classes. When a string is provided, it must fully match - the element's `class` attribute. To match individual classes or perform partial matches, use a regular expression: + the element's `class` attribute. To match individual classes use `locator_assertions.to_contain_class()`. **Usage** @@ -19145,8 +19352,8 @@ async def to_have_class( from playwright.async_api import expect locator = page.locator(\"#component\") - await expect(locator).to_have_class(re.compile(r\"(^|\\\\s)selected(\\\\s|$)\")) await expect(locator).to_have_class(\"middle selected row\") + await expect(locator).to_have_class(re.compile(r\"(^|\\\\s)selected(\\\\s|$)\")) ``` When an array is passed, the method asserts that the list of elements located matches the corresponding list of @@ -19156,7 +19363,7 @@ async def to_have_class( ```py from playwright.async_api import expect - locator = page.locator(\"list > .component\") + locator = page.locator(\".list > .component\") await expect(locator).to_have_class([\"component\", \"component selected\", \"component\"]) ``` @@ -19206,6 +19413,92 @@ async def not_to_have_class( ) ) + async def to_contain_class( + self, + expected: typing.Union[typing.Sequence[str], str], + *, + timeout: typing.Optional[float] = None, + ) -> None: + """LocatorAssertions.to_contain_class + + Ensures the `Locator` points to an element with given CSS classes. All classes from the asserted value, separated + by spaces, must be present in the + [Element.classList](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList) in any order. + + **Usage** + + ```html +
+ ``` + + ```py + from playwright.async_api import expect + + locator = page.locator(\"#component\") + await expect(locator).to_contain_class(\"middle selected row\") + await expect(locator).to_contain_class(\"selected\") + await expect(locator).to_contain_class(\"row middle\") + ``` + + When an array is passed, the method asserts that the list of elements located matches the corresponding list of + expected class lists. Each element's class attribute is matched against the corresponding class in the array: + + ```html +
+
+
+
+
+ ``` + + ```py + from playwright.async_api import expect + + locator = page.locator(\".list > .component\") + await expect(locator).to_contain_class([\"inactive\", \"active\", \"inactive\"]) + ``` + + Parameters + ---------- + expected : Union[Sequence[str], str] + A string containing expected class names, separated by spaces, or a list of such strings to assert multiple + elements. + timeout : Union[float, None] + Time to retry the assertion for in milliseconds. Defaults to `5000`. + """ + __tracebackhide__ = True + + return mapping.from_maybe_impl( + await self._impl_obj.to_contain_class( + expected=mapping.to_impl(expected), timeout=timeout + ) + ) + + async def not_to_contain_class( + self, + expected: typing.Union[typing.Sequence[str], str], + *, + timeout: typing.Optional[float] = None, + ) -> None: + """LocatorAssertions.not_to_contain_class + + The opposite of `locator_assertions.to_contain_class()`. + + Parameters + ---------- + expected : Union[Sequence[str], str] + Expected class or RegExp or a list of those. + timeout : Union[float, None] + Time to retry the assertion for in milliseconds. Defaults to `5000`. + """ + __tracebackhide__ = True + + return mapping.from_maybe_impl( + await self._impl_obj.not_to_contain_class( + expected=mapping.to_impl(expected), timeout=timeout + ) + ) + async def to_have_count( self, count: int, *, timeout: typing.Optional[float] = None ) -> None: diff --git a/playwright/sync_api/__init__.py b/playwright/sync_api/__init__.py index 136433982..53dee2cad 100644 --- a/playwright/sync_api/__init__.py +++ b/playwright/sync_api/__init__.py @@ -30,7 +30,6 @@ from playwright._impl._assertions import PageAssertions as PageAssertionsImpl from playwright.sync_api._context_manager import PlaywrightContextManager from playwright.sync_api._generated import ( - Accessibility, APIRequest, APIRequestContext, APIResponse, @@ -79,6 +78,7 @@ ResourceTiming = playwright._impl._api_structures.ResourceTiming SourceLocation = playwright._impl._api_structures.SourceLocation StorageState = playwright._impl._api_structures.StorageState +StorageStateCookie = playwright._impl._api_structures.StorageStateCookie ViewportSize = playwright._impl._api_structures.ViewportSize Error = playwright._impl._errors.Error @@ -148,7 +148,6 @@ def __call__( __all__ = [ "expect", - "Accessibility", "APIRequest", "APIRequestContext", "APIResponse", @@ -186,6 +185,7 @@ def __call__( "Selectors", "SourceLocation", "StorageState", + "StorageStateCookie", "sync_playwright", "TimeoutError", "Touchscreen", diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index 619319910..82c7b983f 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -18,7 +18,6 @@ import typing from typing import Literal -from playwright._impl._accessibility import Accessibility as AccessibilityImpl from playwright._impl._api_structures import ( ClientCertificate, Cookie, @@ -114,6 +113,24 @@ def resource_type(self) -> str: """ return mapping.from_maybe_impl(self._impl_obj.resource_type) + @property + def service_worker(self) -> typing.Optional["Worker"]: + """Request.service_worker + + The Service `Worker` that is performing the request. + + **Details** + + This method is Chromium only. It's safe to call when using other browsers, but it will always be `null`. + + Requests originated in a Service Worker do not have a `request.frame()` available. + + Returns + ------- + Union[Worker, None] + """ + return mapping.from_impl_nullable(self._impl_obj.service_worker) + @property def method(self) -> str: """Request.method @@ -682,7 +699,7 @@ def fulfill( headers: typing.Optional[typing.Dict[str, str]] = None, body: typing.Optional[typing.Union[str, bytes]] = None, json: typing.Optional[typing.Any] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content_type: typing.Optional[str] = None, response: typing.Optional["APIResponse"] = None, ) -> None: @@ -943,6 +960,10 @@ def handle(route, request): `route.continue_()` will immediately send the request to the network, other matching handlers won't be invoked. Use `route.fallback()` If you want next matching handler in the chain to be invoked. + **NOTE** The `Cookie` header cannot be overridden using this method. If a value is provided, it will be ignored, + and the cookie will be loaded from the browser's cookie store. To set custom cookies, use + `browser_context.add_cookies()`. + Parameters ---------- url : Union[str, None] @@ -1452,7 +1473,8 @@ def move(self, x: float, y: float, *, steps: typing.Optional[int] = None) -> Non y : float Y coordinate relative to the main frame's viewport in CSS pixels. steps : Union[int, None] - Defaults to 1. Sends intermediate `mousemove` events. + Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor + position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. """ return mapping.from_maybe_impl( @@ -2090,6 +2112,7 @@ def click( force: typing.Optional[bool] = None, no_wait_after: typing.Optional[bool] = None, trial: typing.Optional[bool] = None, + steps: typing.Optional[int] = None, ) -> None: """ElementHandle.click @@ -2132,6 +2155,9 @@ def click( trial : Union[bool, None] When set, this method only performs the [actionability](../actionability.md) checks and skips the action. Defaults to `false`. Useful to wait until the element is ready for the action without performing it. + steps : Union[int, None] + Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor + position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. """ return mapping.from_maybe_impl( @@ -2146,6 +2172,7 @@ def click( force=force, noWaitAfter=no_wait_after, trial=trial, + steps=steps, ) ) ) @@ -2163,6 +2190,7 @@ def dblclick( force: typing.Optional[bool] = None, no_wait_after: typing.Optional[bool] = None, trial: typing.Optional[bool] = None, + steps: typing.Optional[int] = None, ) -> None: """ElementHandle.dblclick @@ -2202,6 +2230,9 @@ def dblclick( trial : Union[bool, None] When set, this method only performs the [actionability](../actionability.md) checks and skips the action. Defaults to `false`. Useful to wait until the element is ready for the action without performing it. + steps : Union[int, None] + Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor + position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. """ return mapping.from_maybe_impl( @@ -2215,6 +2246,7 @@ def dblclick( force=force, noWaitAfter=no_wait_after, trial=trial, + steps=steps, ) ) ) @@ -2800,7 +2832,7 @@ def screenshot( *, timeout: typing.Optional[float] = None, type: typing.Optional[Literal["jpeg", "png"]] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, quality: typing.Optional[int] = None, omit_background: typing.Optional[bool] = None, animations: typing.Optional[Literal["allow", "disabled"]] = None, @@ -3130,74 +3162,6 @@ def wait_for_selector( mapping.register(ElementHandleImpl, ElementHandle) -class Accessibility(SyncBase): - - def snapshot( - self, - *, - interesting_only: typing.Optional[bool] = None, - root: typing.Optional["ElementHandle"] = None, - ) -> typing.Optional[typing.Dict]: - """Accessibility.snapshot - - Captures the current state of the accessibility tree. The returned object represents the root accessible node of - the page. - - **NOTE** The Chromium accessibility tree contains nodes that go unused on most platforms and by most screen - readers. Playwright will discard them as well for an easier to process tree, unless `interestingOnly` is set to - `false`. - - **Usage** - - An example of dumping the entire accessibility tree: - - ```py - snapshot = page.accessibility.snapshot() - print(snapshot) - ``` - - An example of logging the focused node's name: - - ```py - def find_focused_node(node): - if node.get(\"focused\"): - return node - for child in (node.get(\"children\") or []): - found_node = find_focused_node(child) - if found_node: - return found_node - return None - - snapshot = page.accessibility.snapshot() - node = find_focused_node(snapshot) - if node: - print(node[\"name\"]) - ``` - - Parameters - ---------- - interesting_only : Union[bool, None] - Prune uninteresting nodes from the tree. Defaults to `true`. - root : Union[ElementHandle, None] - The root DOM element for the snapshot. Defaults to the whole page. - - Returns - ------- - Union[Dict, None] - """ - - return mapping.from_maybe_impl( - self._sync( - self._impl_obj.snapshot( - interestingOnly=interesting_only, root=mapping.to_impl(root) - ) - ) - ) - - -mapping.register(AccessibilityImpl, Accessibility) - - class FileChooser(SyncBase): @property @@ -3976,13 +3940,7 @@ def is_enabled( ) ) - def is_hidden( - self, - selector: str, - *, - strict: typing.Optional[bool] = None, - timeout: typing.Optional[float] = None, - ) -> bool: + def is_hidden(self, selector: str, *, strict: typing.Optional[bool] = None) -> bool: """Frame.is_hidden Returns whether the element is hidden, the opposite of [visible](https://playwright.dev/python/docs/actionability#visible). `selector` that @@ -3996,8 +3954,6 @@ def is_hidden( strict : Union[bool, None] When true, the call requires selector to resolve to a single element. If given selector resolves to more than one element, the call throws an exception. - timeout : Union[float, None] - Deprecated: This option is ignored. `frame.is_hidden()` does not wait for the element to become hidden and returns immediately. Returns ------- @@ -4005,19 +3961,11 @@ def is_hidden( """ return mapping.from_maybe_impl( - self._sync( - self._impl_obj.is_hidden( - selector=selector, strict=strict, timeout=timeout - ) - ) + self._sync(self._impl_obj.is_hidden(selector=selector, strict=strict)) ) def is_visible( - self, - selector: str, - *, - strict: typing.Optional[bool] = None, - timeout: typing.Optional[float] = None, + self, selector: str, *, strict: typing.Optional[bool] = None ) -> bool: """Frame.is_visible @@ -4032,8 +3980,6 @@ def is_visible( strict : Union[bool, None] When true, the call requires selector to resolve to a single element. If given selector resolves to more than one element, the call throws an exception. - timeout : Union[float, None] - Deprecated: This option is ignored. `frame.is_visible()` does not wait for the element to become visible and returns immediately. Returns ------- @@ -4041,11 +3987,7 @@ def is_visible( """ return mapping.from_maybe_impl( - self._sync( - self._impl_obj.is_visible( - selector=selector, strict=strict, timeout=timeout - ) - ) + self._sync(self._impl_obj.is_visible(selector=selector, strict=strict)) ) def dispatch_event( @@ -4287,7 +4229,7 @@ def add_script_tag( self, *, url: typing.Optional[str] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content: typing.Optional[str] = None, type: typing.Optional[str] = None, ) -> "ElementHandle": @@ -4327,7 +4269,7 @@ def add_style_tag( self, *, url: typing.Optional[str] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content: typing.Optional[str] = None, ) -> "ElementHandle": """Frame.add_style_tag @@ -4659,8 +4601,8 @@ def locator( self, selector: str, *, - has_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - has_not_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + has_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + has_not_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, has: typing.Optional["Locator"] = None, has_not: typing.Optional["Locator"] = None, ) -> "Locator": @@ -4927,7 +4869,7 @@ def get_by_role( expanded: typing.Optional[bool] = None, include_hidden: typing.Optional[bool] = None, level: typing.Optional[int] = None, - name: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + name: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, pressed: typing.Optional[bool] = None, selected: typing.Optional[bool] = None, exact: typing.Optional[bool] = None, @@ -5466,6 +5408,7 @@ def drag_and_drop( strict: typing.Optional[bool] = None, timeout: typing.Optional[float] = None, trial: typing.Optional[bool] = None, + steps: typing.Optional[int] = None, ) -> None: """Frame.drag_and_drop @@ -5497,6 +5440,9 @@ def drag_and_drop( trial : Union[bool, None] When set, this method only performs the [actionability](../actionability.md) checks and skips the action. Defaults to `false`. Useful to wait until the element is ready for the action without performing it. + steps : Union[int, None] + Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between the `mousedown` and `mouseup` + of the drag. When set to 1, emits a single `mousemove` event at the destination location. """ return mapping.from_maybe_impl( @@ -5511,6 +5457,7 @@ def drag_and_drop( strict=strict, timeout=timeout, trial=trial, + steps=steps, ) ) ) @@ -6171,8 +6118,8 @@ def locator( self, selector_or_locator: typing.Union["Locator", str], *, - has_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - has_not_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + has_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + has_not_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, has: typing.Optional["Locator"] = None, has_not: typing.Optional["Locator"] = None, ) -> "Locator": @@ -6436,7 +6383,7 @@ def get_by_role( expanded: typing.Optional[bool] = None, include_hidden: typing.Optional[bool] = None, level: typing.Optional[int] = None, - name: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + name: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, pressed: typing.Optional[bool] = None, selected: typing.Optional[bool] = None, exact: typing.Optional[bool] = None, @@ -6726,20 +6673,42 @@ def nth(self, index: int) -> "FrameLocator": class Worker(SyncBase): + @typing.overload def on( self, event: Literal["close"], f: typing.Callable[["Worker"], "None"] ) -> None: """ Emitted when this dedicated [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is terminated.""" + + @typing.overload + def on( + self, event: Literal["console"], f: typing.Callable[["ConsoleMessage"], "None"] + ) -> None: + """ + Emitted when JavaScript within the worker calls one of console API methods, e.g. `console.log` or `console.dir`. + """ + + def on(self, event: str, f: typing.Callable[..., None]) -> None: return super().on(event=event, f=f) + @typing.overload def once( self, event: Literal["close"], f: typing.Callable[["Worker"], "None"] ) -> None: """ Emitted when this dedicated [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is terminated.""" + + @typing.overload + def once( + self, event: Literal["console"], f: typing.Callable[["ConsoleMessage"], "None"] + ) -> None: + """ + Emitted when JavaScript within the worker calls one of console API methods, e.g. `console.log` or `console.dir`. + """ + + def once(self, event: str, f: typing.Callable[..., None]) -> None: return super().once(event=event, f=f) @property @@ -6819,6 +6788,47 @@ def evaluate_handle( ) ) + def expect_event( + self, + event: str, + predicate: typing.Optional[typing.Callable] = None, + *, + timeout: typing.Optional[float] = None, + ) -> EventContextManager: + """Worker.expect_event + + Waits for event to fire and passes its value into the predicate function. Returns when the predicate returns truthy + value. Will throw an error if the page is closed before the event is fired. Returns the event data value. + + **Usage** + + ```py + with worker.expect_event(\"console\") as event_info: + worker.evaluate(\"console.log(42)\") + message = event_info.value + ``` + + Parameters + ---------- + event : str + Event name, same one typically passed into `*.on(event)`. + predicate : Union[Callable, None] + Receives the event data and resolves to truthy value when the waiting should resolve. + timeout : Union[float, None] + Maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The + default value can be changed by using the `browser_context.set_default_timeout()`. + + Returns + ------- + EventContextManager + """ + return EventContextManager( + self, + self._impl_obj.expect_event( + event=event, predicate=self._wrap_handler(predicate), timeout=timeout + ).future, + ) + mapping.register(WorkerImpl, Worker) @@ -6830,7 +6840,7 @@ def register( name: str, script: typing.Optional[str] = None, *, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content_script: typing.Optional[bool] = None, ) -> None: """Selectors.register @@ -7101,16 +7111,33 @@ def set_system_time( class ConsoleMessage(SyncBase): @property - def type(self) -> str: + def type( + self, + ) -> typing.Union[ + Literal["assert"], + Literal["clear"], + Literal["count"], + Literal["debug"], + Literal["dir"], + Literal["dirxml"], + Literal["endGroup"], + Literal["error"], + Literal["info"], + Literal["log"], + Literal["profile"], + Literal["profileEnd"], + Literal["startGroup"], + Literal["startGroupCollapsed"], + Literal["table"], + Literal["timeEnd"], + Literal["trace"], + Literal["warning"], + ]: """ConsoleMessage.type - One of the following values: `'log'`, `'debug'`, `'info'`, `'error'`, `'warning'`, `'dir'`, `'dirxml'`, `'table'`, - `'trace'`, `'clear'`, `'startGroup'`, `'startGroupCollapsed'`, `'endGroup'`, `'assert'`, `'profile'`, - `'profileEnd'`, `'count'`, `'timeEnd'`. - Returns ------- - str + Union["assert", "clear", "count", "debug", "dir", "dirxml", "endGroup", "error", "info", "log", "profile", "profileEnd", "startGroup", "startGroupCollapsed", "table", "timeEnd", "trace", "warning"] """ return mapping.from_maybe_impl(self._impl_obj.type) @@ -7160,6 +7187,19 @@ def page(self) -> typing.Optional["Page"]: """ return mapping.from_impl_nullable(self._impl_obj.page) + @property + def worker(self) -> typing.Optional["Worker"]: + """ConsoleMessage.worker + + The web worker or service worker that produced this console message, if any. Note that console messages from web + workers also have non-null `console_message.page()`. + + Returns + ------- + Union[Worker, None] + """ + return mapping.from_impl_nullable(self._impl_obj.worker) + mapping.register(ConsoleMessageImpl, ConsoleMessage) @@ -7830,16 +7870,6 @@ def once( def once(self, event: str, f: typing.Callable[..., None]) -> None: return super().once(event=event, f=f) - @property - def accessibility(self) -> "Accessibility": - """Page.accessibility - - Returns - ------- - Accessibility - """ - return mapping.from_impl(self._impl_obj.accessibility) - @property def keyboard(self) -> "Keyboard": """Page.keyboard @@ -8687,7 +8717,7 @@ def add_script_tag( self, *, url: typing.Optional[str] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content: typing.Optional[str] = None, type: typing.Optional[str] = None, ) -> "ElementHandle": @@ -8726,7 +8756,7 @@ def add_style_tag( self, *, url: typing.Optional[str] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content: typing.Optional[str] = None, ) -> "ElementHandle": """Page.add_style_tag @@ -9424,7 +9454,7 @@ def add_init_script( self, script: typing.Optional[str] = None, *, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, ) -> None: """Page.add_init_script @@ -9529,8 +9559,8 @@ def handle_route(route: Route): Parameters ---------- url : Union[Callable[[str], bool], Pattern[str], str] - A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a `baseURL` via the context - options was provided and the passed URL is a path, it gets merged via the + A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If `baseURL` is set in + the context options and the provided URL is a string that does not start with `*`, it is resolved using the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. handler : Union[Callable[[Route, Request], Any], Callable[[Route], Any]] handler function to route the request. @@ -9652,7 +9682,7 @@ def route_from_har( self, har: typing.Union[pathlib.Path, str], *, - url: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + url: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, not_found: typing.Optional[Literal["abort", "fallback"]] = None, update: typing.Optional[bool] = None, update_content: typing.Optional[Literal["attach", "embed"]] = None, @@ -9710,7 +9740,7 @@ def screenshot( *, timeout: typing.Optional[float] = None, type: typing.Optional[Literal["jpeg", "png"]] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, quality: typing.Optional[int] = None, omit_background: typing.Optional[bool] = None, full_page: typing.Optional[bool] = None, @@ -10156,8 +10186,8 @@ def locator( self, selector: str, *, - has_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - has_not_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + has_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + has_not_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, has: typing.Optional["Locator"] = None, has_not: typing.Optional["Locator"] = None, ) -> "Locator": @@ -10422,7 +10452,7 @@ def get_by_role( expanded: typing.Optional[bool] = None, include_hidden: typing.Optional[bool] = None, level: typing.Optional[int] = None, - name: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + name: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, pressed: typing.Optional[bool] = None, selected: typing.Optional[bool] = None, exact: typing.Optional[bool] = None, @@ -10961,6 +10991,7 @@ def drag_and_drop( timeout: typing.Optional[float] = None, strict: typing.Optional[bool] = None, trial: typing.Optional[bool] = None, + steps: typing.Optional[int] = None, ) -> None: """Page.drag_and_drop @@ -11008,6 +11039,9 @@ def drag_and_drop( trial : Union[bool, None] When set, this method only performs the [actionability](../actionability.md) checks and skips the action. Defaults to `false`. Useful to wait until the element is ready for the action without performing it. + steps : Union[int, None] + Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between the `mousedown` and `mouseup` + of the drag. When set to 1, emits a single `mousemove` event at the destination location. """ return mapping.from_maybe_impl( @@ -11022,6 +11056,7 @@ def drag_and_drop( timeout=timeout, strict=strict, trial=trial, + steps=steps, ) ) ) @@ -11570,8 +11605,8 @@ def run(playwright: Playwright): def pause(self) -> None: """Page.pause - Pauses script execution. Playwright will stop executing the script and wait for the user to either press 'Resume' - button in the page overlay or to call `playwright.resume()` in the DevTools console. + Pauses script execution. Playwright will stop executing the script and wait for the user to either press the + 'Resume' button in the page overlay or to call `playwright.resume()` in the DevTools console. User can inspect selectors or perform manual steps while paused. Resume will continue running the original script from the place it was paused. @@ -11596,7 +11631,7 @@ def pdf( height: typing.Optional[typing.Union[str, float]] = None, prefer_css_page_size: typing.Optional[bool] = None, margin: typing.Optional[PdfMargins] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, outline: typing.Optional[bool] = None, tagged: typing.Optional[bool] = None, ) -> bytes: @@ -12256,7 +12291,7 @@ def add_locator_handler( ```py # Setup the handler. - def handler(): + async def handler(): await page.get_by_role(\"button\", name=\"No thanks\").click() await page.add_locator_handler(page.get_by_text(\"Sign up to the newsletter\"), handler) @@ -12269,7 +12304,7 @@ def handler(): ```py # Setup the handler. - def handler(): + async def handler(): await page.get_by_role(\"button\", name=\"Remind me later\").click() await page.add_locator_handler(page.get_by_text(\"Confirm your security details\"), handler) @@ -12284,7 +12319,7 @@ def handler(): ```py # Setup the handler. - def handler(): + async def handler(): await page.evaluate(\"window.removeObstructionsForTestIfNeeded()\") await page.add_locator_handler(page.locator(\"body\"), handler, no_wait_after=True) @@ -12297,7 +12332,7 @@ def handler(): invocations by setting `times`: ```py - def handler(locator): + async def handler(locator): await locator.click() await page.add_locator_handler(page.get_by_label(\"Close\"), handler, times=1) ``` @@ -12343,6 +12378,49 @@ def remove_locator_handler(self, locator: "Locator") -> None: self._sync(self._impl_obj.remove_locator_handler(locator=locator._impl_obj)) ) + def requests(self) -> typing.List["Request"]: + """Page.requests + + Returns up to (currently) 100 last network request from this page. See `page.on('request')` for more details. + + Returned requests should be accessed immediately, otherwise they might be collected to prevent unbounded memory + growth as new requests come in. Once collected, retrieving most information about the request is impossible. + + Note that requests reported through the `page.on('request')` request are not collected, so there is a trade off + between efficient memory usage with `page.requests()` and the amount of available information reported + through `page.on('request')`. + + Returns + ------- + List[Request] + """ + + return mapping.from_impl_list(self._sync(self._impl_obj.requests())) + + def console_messages(self) -> typing.List["ConsoleMessage"]: + """Page.console_messages + + Returns up to (currently) 200 last console messages from this page. See `page.on('console')` for more details. + + Returns + ------- + List[ConsoleMessage] + """ + + return mapping.from_impl_list(self._sync(self._impl_obj.console_messages())) + + def page_errors(self) -> typing.List["Error"]: + """Page.page_errors + + Returns up to (currently) 200 last page errors from this page. See `page.on('page_error')` for more details. + + Returns + ------- + List[Error] + """ + + return mapping.from_impl_list(self._sync(self._impl_obj.page_errors())) + mapping.register(PageImpl, Page) @@ -12384,13 +12462,7 @@ def on( self, event: Literal["backgroundpage"], f: typing.Callable[["Page"], "None"] ) -> None: """ - **NOTE** Only works with Chromium browser's persistent context. - - Emitted when new background page is created in the context. - - ```py - background_page = context.wait_for_event(\"backgroundpage\") - ```""" + This event is not emitted.""" @typing.overload def on( @@ -12530,13 +12602,7 @@ def once( self, event: Literal["backgroundpage"], f: typing.Callable[["Page"], "None"] ) -> None: """ - **NOTE** Only works with Chromium browser's persistent context. - - Emitted when new background page is created in the context. - - ```py - background_page = context.wait_for_event(\"backgroundpage\") - ```""" + This event is not emitted.""" @typing.overload def once( @@ -12689,7 +12755,8 @@ def pages(self) -> typing.List["Page"]: def browser(self) -> typing.Optional["Browser"]: """BrowserContext.browser - Returns the browser instance of the context. If it was launched as a persistent context null gets returned. + Gets the browser instance that owns the context. Returns `null` if the context is created outside of normal + browser, e.g. Android or Electron. Returns ------- @@ -12701,9 +12768,7 @@ def browser(self) -> typing.Optional["Browser"]: def background_pages(self) -> typing.List["Page"]: """BrowserContext.background_pages - **NOTE** Background pages are only supported on Chromium-based browsers. - - All existing background pages in the context. + Returns an empty list. Returns ------- @@ -12829,7 +12894,7 @@ def cookies( Returns ------- - List[{name: str, value: str, domain: str, path: str, expires: float, httpOnly: bool, secure: bool, sameSite: Union["Lax", "None", "Strict"]}] + List[{name: str, value: str, domain: str, path: str, expires: float, httpOnly: bool, secure: bool, sameSite: Union["Lax", "None", "Strict"], partitionKey: Union[str, None]}] """ return mapping.from_impl_list( @@ -12850,7 +12915,7 @@ def add_cookies(self, cookies: typing.Sequence[SetCookieParam]) -> None: Parameters ---------- - cookies : Sequence[{name: str, value: str, url: Union[str, None], domain: Union[str, None], path: Union[str, None], expires: Union[float, None], httpOnly: Union[bool, None], secure: Union[bool, None], sameSite: Union["Lax", "None", "Strict", None]}] + cookies : Sequence[{name: str, value: str, url: Union[str, None], domain: Union[str, None], path: Union[str, None], expires: Union[float, None], httpOnly: Union[bool, None], secure: Union[bool, None], sameSite: Union["Lax", "None", "Strict", None], partitionKey: Union[str, None]}] """ return mapping.from_maybe_impl( @@ -12860,9 +12925,9 @@ def add_cookies(self, cookies: typing.Sequence[SetCookieParam]) -> None: def clear_cookies( self, *, - name: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - domain: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - path: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + name: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + domain: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + path: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, ) -> None: """BrowserContext.clear_cookies @@ -12919,6 +12984,8 @@ def grant_permissions( - `'clipboard-write'` - `'geolocation'` - `'gyroscope'` + - `'local-fonts'` + - `'local-network-access'` - `'magnetometer'` - `'microphone'` - `'midi-sysex'` (system-exclusive midi) @@ -13017,7 +13084,7 @@ def add_init_script( self, script: typing.Optional[str] = None, *, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, ) -> None: """BrowserContext.add_init_script @@ -13245,8 +13312,8 @@ def handle_route(route: Route): Parameters ---------- url : Union[Callable[[str], bool], Pattern[str], str] - A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a `baseURL` via the context - options was provided and the passed URL is a path, it gets merged via the + A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If `baseURL` is set in + the context options and the provided URL is a string that does not start with `*`, it is resolved using the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. handler : Union[Callable[[Route, Request], Any], Callable[[Route], Any]] handler function to route the request. @@ -13371,7 +13438,7 @@ def route_from_har( self, har: typing.Union[pathlib.Path, str], *, - url: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + url: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, not_found: typing.Optional[Literal["abort", "fallback"]] = None, update: typing.Optional[bool] = None, update_content: typing.Optional[Literal["attach", "embed"]] = None, @@ -13483,7 +13550,7 @@ def close(self, *, reason: typing.Optional[str] = None) -> None: def storage_state( self, *, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, indexed_db: typing.Optional[bool] = None, ) -> StorageState: """BrowserContext.storage_state @@ -13501,9 +13568,6 @@ def storage_state( state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase Authentication, enable this. - **NOTE** IndexedDBs with typed arrays are currently not supported. - - Returns ------- {cookies: List[{name: str, value: str, domain: str, path: str, expires: float, httpOnly: bool, secure: bool, sameSite: Union["Lax", "None", "Strict"]}], origins: List[{origin: str, localStorage: List[{name: str, value: str}]}]} @@ -13788,9 +13852,9 @@ def new_context( accept_downloads: typing.Optional[bool] = None, default_browser_type: typing.Optional[str] = None, proxy: typing.Optional[ProxySettings] = None, - record_har_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_har_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_har_omit_content: typing.Optional[bool] = None, - record_video_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_video_dir: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_video_size: typing.Optional[ViewportSize] = None, storage_state: typing.Optional[ typing.Union[StorageState, str, pathlib.Path] @@ -13799,7 +13863,7 @@ def new_context( strict_selectors: typing.Optional[bool] = None, service_workers: typing.Optional[Literal["allow", "block"]] = None, record_har_url_filter: typing.Optional[ - typing.Union[str, typing.Pattern[str]] + typing.Union[typing.Pattern[str], str] ] = None, record_har_mode: typing.Optional[Literal["full", "minimal"]] = None, record_har_content: typing.Optional[Literal["attach", "embed", "omit"]] = None, @@ -13954,6 +14018,10 @@ def new_context( `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided with an exact match to the request origin that the certificate is valid for. + Client certificate authentication is only active when at least one client certificate is provided. If you want to + reject all client certificates sent by the server, you need to provide a client certificate with an `origin` that + does not match any of the domains you plan to visit. + **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it work by replacing `localhost` with `local.playwright`. @@ -14037,9 +14105,9 @@ def new_page( accept_downloads: typing.Optional[bool] = None, default_browser_type: typing.Optional[str] = None, proxy: typing.Optional[ProxySettings] = None, - record_har_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_har_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_har_omit_content: typing.Optional[bool] = None, - record_video_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_video_dir: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_video_size: typing.Optional[ViewportSize] = None, storage_state: typing.Optional[ typing.Union[StorageState, str, pathlib.Path] @@ -14048,7 +14116,7 @@ def new_page( strict_selectors: typing.Optional[bool] = None, service_workers: typing.Optional[Literal["allow", "block"]] = None, record_har_url_filter: typing.Optional[ - typing.Union[str, typing.Pattern[str]] + typing.Union[typing.Pattern[str], str] ] = None, record_har_mode: typing.Optional[Literal["full", "minimal"]] = None, record_har_content: typing.Optional[Literal["attach", "embed", "omit"]] = None, @@ -14187,6 +14255,10 @@ def new_page( `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided with an exact match to the request origin that the certificate is valid for. + Client certificate authentication is only active when at least one client certificate is provided. If you want to + reject all client certificates sent by the server, you need to provide a client certificate with an `origin` that + does not match any of the domains you plan to visit. + **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it work by replacing `localhost` with `local.playwright`. @@ -14280,7 +14352,7 @@ def start_tracing( self, *, page: typing.Optional["Page"] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, screenshots: typing.Optional[bool] = None, categories: typing.Optional[typing.Sequence[str]] = None, ) -> None: @@ -14375,7 +14447,7 @@ def executable_path(self) -> str: def launch( self, *, - executable_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + executable_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, channel: typing.Optional[str] = None, args: typing.Optional[typing.Sequence[str]] = None, ignore_default_args: typing.Optional[ @@ -14389,9 +14461,9 @@ def launch( headless: typing.Optional[bool] = None, devtools: typing.Optional[bool] = None, proxy: typing.Optional[ProxySettings] = None, - downloads_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + downloads_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, slow_mo: typing.Optional[float] = None, - traces_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, + traces_dir: typing.Optional[typing.Union[pathlib.Path, str]] = None, chromium_sandbox: typing.Optional[bool] = None, firefox_user_prefs: typing.Optional[ typing.Dict[str, typing.Union[str, float, bool]] @@ -14461,7 +14533,7 @@ def launch( headless : Union[bool, None] Whether to run browser in headless mode. More details for [Chromium](https://developers.google.com/web/updates/2017/04/headless-chrome) and - [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode). Defaults to `true` unless the + [Firefox](https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/). Defaults to `true` unless the `devtools` option is `true`. devtools : Union[bool, None] **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the @@ -14484,6 +14556,9 @@ def launch( Firefox user preferences. Learn more about the Firefox user preferences at [`about:config`](https://support.mozilla.org/en-US/kb/about-config-editor-firefox). + You can also provide a path to a custom [`policies.json` file](https://mozilla.github.io/policy-templates/) via + `PLAYWRIGHT_FIREFOX_POLICIES_JSON` environment variable. + Returns ------- Browser @@ -14518,7 +14593,7 @@ def launch_persistent_context( user_data_dir: typing.Union[str, pathlib.Path], *, channel: typing.Optional[str] = None, - executable_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + executable_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, args: typing.Optional[typing.Sequence[str]] = None, ignore_default_args: typing.Optional[ typing.Union[bool, typing.Sequence[str]] @@ -14531,7 +14606,7 @@ def launch_persistent_context( headless: typing.Optional[bool] = None, devtools: typing.Optional[bool] = None, proxy: typing.Optional[ProxySettings] = None, - downloads_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + downloads_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, slow_mo: typing.Optional[float] = None, viewport: typing.Optional[ViewportSize] = None, screen: typing.Optional[ViewportSize] = None, @@ -14559,20 +14634,20 @@ def launch_persistent_context( forced_colors: typing.Optional[Literal["active", "none", "null"]] = None, contrast: typing.Optional[Literal["more", "no-preference", "null"]] = None, accept_downloads: typing.Optional[bool] = None, - traces_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, + traces_dir: typing.Optional[typing.Union[pathlib.Path, str]] = None, chromium_sandbox: typing.Optional[bool] = None, firefox_user_prefs: typing.Optional[ typing.Dict[str, typing.Union[str, float, bool]] ] = None, - record_har_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_har_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_har_omit_content: typing.Optional[bool] = None, - record_video_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_video_dir: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_video_size: typing.Optional[ViewportSize] = None, base_url: typing.Optional[str] = None, strict_selectors: typing.Optional[bool] = None, service_workers: typing.Optional[Literal["allow", "block"]] = None, record_har_url_filter: typing.Optional[ - typing.Union[str, typing.Pattern[str]] + typing.Union[typing.Pattern[str], str] ] = None, record_har_mode: typing.Optional[Literal["full", "minimal"]] = None, record_har_content: typing.Optional[Literal["attach", "embed", "omit"]] = None, @@ -14588,11 +14663,22 @@ def launch_persistent_context( Parameters ---------- user_data_dir : Union[pathlib.Path, str] - Path to a User Data Directory, which stores browser session data like cookies and local storage. More details for + Path to a User Data Directory, which stores browser session data like cookies and local storage. Pass an empty + string to create a temporary directory. + + More details for [Chromium](https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md#introduction) and - [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options#User_Profile). Note that Chromium's - user data directory is the **parent** directory of the "Profile Path" seen at `chrome://version`. Pass an empty - string to use a temporary directory instead. + [Firefox](https://wiki.mozilla.org/Firefox/CommandLineOptions#User_profile). Chromium's user data directory is the + **parent** directory of the "Profile Path" seen at `chrome://version`. + + Note that browsers do not allow launching multiple instances with the same User Data Directory. + + **NOTE** Chromium/Chrome: Due to recent Chrome policy changes, automating the default Chrome user profile is not + supported. Pointing `userDataDir` to Chrome's main "User Data" directory (the profile used for your regular + browsing) may result in pages not loading or the browser exiting. Create and use a separate directory (for example, + an empty folder) as your automation profile instead. See https://developer.chrome.com/blog/remote-debugging-port + for details. + channel : Union[str, None] Browser distribution channel. @@ -14626,7 +14712,7 @@ def launch_persistent_context( headless : Union[bool, None] Whether to run browser in headless mode. More details for [Chromium](https://developers.google.com/web/updates/2017/04/headless-chrome) and - [Firefox](https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Headless_mode). Defaults to `true` unless the + [Firefox](https://hacks.mozilla.org/2017/12/using-headless-mode-in-firefox/). Defaults to `true` unless the `devtools` option is `true`. devtools : Union[bool, None] **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the @@ -14713,6 +14799,9 @@ def launch_persistent_context( firefox_user_prefs : Union[Dict[str, Union[bool, float, str]], None] Firefox user preferences. Learn more about the Firefox user preferences at [`about:config`](https://support.mozilla.org/en-US/kb/about-config-editor-firefox). + + You can also provide a path to a custom [`policies.json` file](https://mozilla.github.io/policy-templates/) via + `PLAYWRIGHT_FIREFOX_POLICIES_JSON` environment variable. record_har_path : Union[pathlib.Path, str, None] Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into the specified HAR file on the filesystem. If not specified, the HAR is not recorded. Make sure to call `browser_context.close()` @@ -14764,6 +14853,10 @@ def launch_persistent_context( `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided with an exact match to the request origin that the certificate is valid for. + Client certificate authentication is only active when at least one client certificate is provided. If you want to + reject all client certificates sent by the server, you need to provide a client certificate with an `origin` that + does not match any of the domains you plan to visit. + **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it work by replacing `localhost` with `local.playwright`. @@ -15090,6 +15183,15 @@ def start( Start tracing. + **NOTE** You probably want to + [enable tracing in your config file](https://playwright.dev/docs/api/class-testoptions#test-options-trace) instead + of using `Tracing.start`. + + The `context.tracing` API captures browser operations and network activity, but it doesn't record test assertions + (like `expect` calls). We recommend + [enabling tracing through Playwright Test configuration](https://playwright.dev/docs/api/class-testoptions#test-options-trace), + which includes those assertions and provides a more complete trace for debugging test failures. + **Usage** ```py @@ -15171,7 +15273,7 @@ def start_chunk( ) def stop_chunk( - self, *, path: typing.Optional[typing.Union[str, pathlib.Path]] = None + self, *, path: typing.Optional[typing.Union[pathlib.Path, str]] = None ) -> None: """Tracing.stop_chunk @@ -15186,7 +15288,7 @@ def stop_chunk( return mapping.from_maybe_impl(self._sync(self._impl_obj.stop_chunk(path=path))) def stop( - self, *, path: typing.Optional[typing.Union[str, pathlib.Path]] = None + self, *, path: typing.Optional[typing.Union[pathlib.Path, str]] = None ) -> None: """Tracing.stop @@ -15316,6 +15418,30 @@ def content_frame(self) -> "FrameLocator": """ return mapping.from_impl(self._impl_obj.content_frame) + @property + def description(self) -> typing.Optional[str]: + """Locator.description + + Returns locator description previously set with `locator.describe()`. Returns `null` if no custom + description has been set. Prefer `Locator.toString()` for a human-readable representation, as it uses the + description when available. + + **Usage** + + ```py + button = page.get_by_role(\"button\").describe(\"Subscribe button\") + print(button.description()) # \"Subscribe button\" + + input = page.get_by_role(\"textbox\") + print(input.description()) # None + ``` + + Returns + ------- + Union[str, None] + """ + return mapping.from_maybe_impl(self._impl_obj.description) + def bounding_box( self, *, timeout: typing.Optional[float] = None ) -> typing.Optional[FloatRect]: @@ -15437,6 +15563,7 @@ def click( force: typing.Optional[bool] = None, no_wait_after: typing.Optional[bool] = None, trial: typing.Optional[bool] = None, + steps: typing.Optional[int] = None, ) -> None: """Locator.click @@ -15501,6 +15628,9 @@ def click( to `false`. Useful to wait until the element is ready for the action without performing it. Note that keyboard `modifiers` will be pressed regardless of `trial` to allow testing elements which are only visible when those keys are pressed. + steps : Union[int, None] + Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor + position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. """ return mapping.from_maybe_impl( @@ -15515,6 +15645,7 @@ def click( force=force, noWaitAfter=no_wait_after, trial=trial, + steps=steps, ) ) ) @@ -15532,6 +15663,7 @@ def dblclick( force: typing.Optional[bool] = None, no_wait_after: typing.Optional[bool] = None, trial: typing.Optional[bool] = None, + steps: typing.Optional[int] = None, ) -> None: """Locator.dblclick @@ -15577,6 +15709,9 @@ def dblclick( to `false`. Useful to wait until the element is ready for the action without performing it. Note that keyboard `modifiers` will be pressed regardless of `trial` to allow testing elements which are only visible when those keys are pressed. + steps : Union[int, None] + Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor + position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location. """ return mapping.from_maybe_impl( @@ -15590,6 +15725,7 @@ def dblclick( force=force, noWaitAfter=no_wait_after, trial=trial, + steps=steps, ) ) ) @@ -15680,6 +15816,13 @@ def evaluate( **Usage** + Passing argument to `expression`: + + ```py + result = page.get_by_testid(\"myId\").evaluate(\"(element, [x, y]) => element.textContent + ' ' + x * y\", [7, 8]) + print(result) # prints \"myId text 56\" + ``` + Parameters ---------- expression : str @@ -15688,8 +15831,8 @@ def evaluate( arg : Union[Any, None] Optional argument to pass to `expression`. timeout : Union[float, None] - Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can - be changed by using the `browser_context.set_default_timeout()` or `page.set_default_timeout()` methods. + Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, + evaluation itself is not limited by the timeout. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. Returns ------- @@ -15782,8 +15925,8 @@ def evaluate_handle( arg : Union[Any, None] Optional argument to pass to `expression`. timeout : Union[float, None] - Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can - be changed by using the `browser_context.set_default_timeout()` or `page.set_default_timeout()` methods. + Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, + evaluation itself is not limited by the timeout. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. Returns ------- @@ -15901,8 +16044,8 @@ def locator( self, selector_or_locator: typing.Union[str, "Locator"], *, - has_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - has_not_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + has_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + has_not_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, has: typing.Optional["Locator"] = None, has_not: typing.Optional["Locator"] = None, ) -> "Locator": @@ -16166,7 +16309,7 @@ def get_by_role( expanded: typing.Optional[bool] = None, include_hidden: typing.Optional[bool] = None, level: typing.Optional[int] = None, - name: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + name: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, pressed: typing.Optional[bool] = None, selected: typing.Optional[bool] = None, exact: typing.Optional[bool] = None, @@ -16498,11 +16641,36 @@ def nth(self, index: int) -> "Locator": return mapping.from_impl(self._impl_obj.nth(index=index)) + def describe(self, description: str) -> "Locator": + """Locator.describe + + Describes the locator, description is used in the trace viewer and reports. Returns the locator pointing to the + same element. + + **Usage** + + ```py + button = page.get_by_test_id(\"btn-sub\").describe(\"Subscribe button\") + button.click() + ``` + + Parameters + ---------- + description : str + Locator description. + + Returns + ------- + Locator + """ + + return mapping.from_impl(self._impl_obj.describe(description=description)) + def filter( self, *, - has_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - has_not_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + has_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + has_not_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, has: typing.Optional["Locator"] = None, has_not: typing.Optional["Locator"] = None, visible: typing.Optional[bool] = None, @@ -16612,7 +16780,7 @@ def and_(self, locator: "Locator") -> "Locator": The following example finds a button with a specific title. ```py - button = page.get_by_role(\"button\").and_(page.getByTitle(\"Subscribe\")) + button = page.get_by_role(\"button\").and_(page.get_by_title(\"Subscribe\")) ``` Parameters @@ -16717,6 +16885,7 @@ def drag_to( trial: typing.Optional[bool] = None, source_position: typing.Optional[Position] = None, target_position: typing.Optional[Position] = None, + steps: typing.Optional[int] = None, ) -> None: """Locator.drag_to @@ -16763,6 +16932,9 @@ def drag_to( target_position : Union[{x: float, y: float}, None] Drops on the target element at this point relative to the top-left corner of the element's padding box. If not specified, some visible point of the element is used. + steps : Union[int, None] + Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between the `mousedown` and `mouseup` + of the drag. When set to 1, emits a single `mousemove` event at the destination location. """ return mapping.from_maybe_impl( @@ -16775,6 +16947,7 @@ def drag_to( trial=trial, sourcePosition=source_position, targetPosition=target_position, + steps=steps, ) ) ) @@ -17201,7 +17374,7 @@ def screenshot( *, timeout: typing.Optional[float] = None, type: typing.Optional[Literal["jpeg", "png"]] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, quality: typing.Optional[int] = None, omit_background: typing.Optional[bool] = None, animations: typing.Optional[Literal["allow", "disabled"]] = None, @@ -18781,7 +18954,7 @@ def fetch( def storage_state( self, *, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, indexed_db: typing.Optional[bool] = None, ) -> StorageState: """APIRequestContext.storage_state @@ -18827,6 +19000,7 @@ def new_context( ] = None, client_certificates: typing.Optional[typing.List[ClientCertificate]] = None, fail_on_status_code: typing.Optional[bool] = None, + max_redirects: typing.Optional[int] = None, ) -> "APIRequestContext": """APIRequest.new_context @@ -18872,12 +19046,20 @@ def new_context( `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided with an exact match to the request origin that the certificate is valid for. + Client certificate authentication is only active when at least one client certificate is provided. If you want to + reject all client certificates sent by the server, you need to provide a client certificate with an `origin` that + does not match any of the domains you plan to visit. + **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it work by replacing `localhost` with `local.playwright`. fail_on_status_code : Union[bool, None] Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes. + max_redirects : Union[int, None] + Maximum number of request redirects that will be followed automatically. An error will be thrown if the number is + exceeded. Defaults to `20`. Pass `0` to not follow redirects. This can be overwritten for each request + individually. Returns ------- @@ -18897,6 +19079,7 @@ def new_context( storageState=storage_state, clientCertificates=client_certificates, failOnStatusCode=fail_on_status_code, + maxRedirects=max_redirects, ) ) ) @@ -19278,7 +19461,7 @@ def to_have_class( """LocatorAssertions.to_have_class Ensures the `Locator` points to an element with given CSS classes. When a string is provided, it must fully match - the element's `class` attribute. To match individual classes or perform partial matches, use a regular expression: + the element's `class` attribute. To match individual classes use `locator_assertions.to_contain_class()`. **Usage** @@ -19290,8 +19473,8 @@ def to_have_class( from playwright.sync_api import expect locator = page.locator(\"#component\") - expect(locator).to_have_class(re.compile(r\"(^|\\\\s)selected(\\\\s|$)\")) expect(locator).to_have_class(\"middle selected row\") + expect(locator).to_have_class(re.compile(r\"(^|\\\\s)selected(\\\\s|$)\")) ``` When an array is passed, the method asserts that the list of elements located matches the corresponding list of @@ -19301,7 +19484,7 @@ def to_have_class( ```py from playwright.sync_api import expect - locator = page.locator(\"list > .component\") + locator = page.locator(\".list > .component\") expect(locator).to_have_class([\"component\", \"component selected\", \"component\"]) ``` @@ -19355,6 +19538,96 @@ def not_to_have_class( ) ) + def to_contain_class( + self, + expected: typing.Union[typing.Sequence[str], str], + *, + timeout: typing.Optional[float] = None, + ) -> None: + """LocatorAssertions.to_contain_class + + Ensures the `Locator` points to an element with given CSS classes. All classes from the asserted value, separated + by spaces, must be present in the + [Element.classList](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList) in any order. + + **Usage** + + ```html +
+ ``` + + ```py + from playwright.sync_api import expect + + locator = page.locator(\"#component\") + expect(locator).to_contain_class(\"middle selected row\") + expect(locator).to_contain_class(\"selected\") + expect(locator).to_contain_class(\"row middle\") + ``` + + When an array is passed, the method asserts that the list of elements located matches the corresponding list of + expected class lists. Each element's class attribute is matched against the corresponding class in the array: + + ```html +
+
+
+
+
+ ``` + + ```py + from playwright.sync_api import expect + + locator = page.locator(\".list > .component\") + await expect(locator).to_contain_class([\"inactive\", \"active\", \"inactive\"]) + ``` + + Parameters + ---------- + expected : Union[Sequence[str], str] + A string containing expected class names, separated by spaces, or a list of such strings to assert multiple + elements. + timeout : Union[float, None] + Time to retry the assertion for in milliseconds. Defaults to `5000`. + """ + __tracebackhide__ = True + + return mapping.from_maybe_impl( + self._sync( + self._impl_obj.to_contain_class( + expected=mapping.to_impl(expected), timeout=timeout + ) + ) + ) + + def not_to_contain_class( + self, + expected: typing.Union[typing.Sequence[str], str], + *, + timeout: typing.Optional[float] = None, + ) -> None: + """LocatorAssertions.not_to_contain_class + + The opposite of `locator_assertions.to_contain_class()`. + + Parameters + ---------- + expected : Union[Sequence[str], str] + Expected class or RegExp or a list of those. + timeout : Union[float, None] + Time to retry the assertion for in milliseconds. Defaults to `5000`. + """ + __tracebackhide__ = True + + return mapping.from_maybe_impl( + self._sync( + self._impl_obj.not_to_contain_class( + expected=mapping.to_impl(expected), timeout=timeout + ) + ) + ) + def to_have_count( self, count: int, *, timeout: typing.Optional[float] = None ) -> None: diff --git a/pyproject.toml b/pyproject.toml index 52ed67370..1ff674eab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools==78.1.0", "setuptools-scm==8.2.0", "wheel==0.45.1", "auditwheel==6.2.0"] +requires = ["setuptools==80.9.0", "setuptools-scm==8.3.1", "wheel==0.45.1", "auditwheel==6.2.0"] build-backend = "setuptools.build_meta" [project] @@ -9,7 +9,7 @@ authors = [ {name = "Microsoft Corporation"} ] readme = "README.md" -license = {text = "Apache-2.0"} +license = "Apache-2.0" dynamic = ["version"] requires-python = ">=3.9" # Please when changing dependencies run the following commands to update requirements.txt: @@ -29,7 +29,6 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", - "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", ] diff --git a/requirements.txt b/requirements.txt index 5298f1ff4..75d362fe8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ # This file was autogenerated by uv via the following command: # uv pip compile pyproject.toml -o requirements.txt -greenlet==3.1.1 +greenlet==3.2.4 # via playwright (pyproject.toml) pyee==13.0.0 # via playwright (pyproject.toml) -typing-extensions==4.12.2 +typing-extensions==4.14.1 # via pyee diff --git a/scripts/documentation_provider.py b/scripts/documentation_provider.py index 6ea931fac..a842a1aad 100644 --- a/scripts/documentation_provider.py +++ b/scripts/documentation_provider.py @@ -13,6 +13,7 @@ # limitations under the License. import json +import pathlib import re import subprocess from sys import stderr @@ -359,6 +360,8 @@ def serialize_python_type(self, value: Any, direction: str) -> str: match = re.match(r"^$", str_value) if match: return match.group(1) + if str_value == str(pathlib.Path): + return "pathlib.Path" match = re.match( r"playwright._impl._event_context_manager.EventContextManagerImpl\[playwright._impl.[^.]+.(.*)\]", str_value, diff --git a/scripts/expected_api_mismatch.txt b/scripts/expected_api_mismatch.txt index c6b3c7a95..f47493440 100644 --- a/scripts/expected_api_mismatch.txt +++ b/scripts/expected_api_mismatch.txt @@ -4,9 +4,6 @@ Parameter not documented: Browser.new_context(default_browser_type=) Parameter not documented: Browser.new_page(default_browser_type=) -# We don't expand the type of the return value here. -Parameter type mismatch in Accessibility.snapshot(return=): documented as Union[{role: str, name: str, value: Union[float, str], description: str, keyshortcuts: str, roledescription: str, valuetext: str, disabled: bool, expanded: bool, focused: bool, modal: bool, multiline: bool, multiselectable: bool, readonly: bool, required: bool, selected: bool, checked: Union["mixed", bool], pressed: Union["mixed", bool], level: int, valuemin: float, valuemax: float, autocomplete: str, haspopup: str, invalid: str, orientation: str, children: List[Dict]}, None], code has Union[Dict, None] - # One vs two arguments in the callback, Python explicitly unions. Parameter type mismatch in BrowserContext.route(handler=): documented as Callable[[Route, Request], Union[Any, Any]], code has Union[Callable[[Route, Request], Any], Callable[[Route], Any]] Parameter type mismatch in BrowserContext.unroute(handler=): documented as Union[Callable[[Route, Request], Union[Any, Any]], None], code has Union[Callable[[Route, Request], Any], Callable[[Route], Any], None] diff --git a/scripts/generate_api.py b/scripts/generate_api.py index 01f8f525a..3d2f45156 100644 --- a/scripts/generate_api.py +++ b/scripts/generate_api.py @@ -18,7 +18,6 @@ from typing import Any, Dict, List, Match, Optional, Union, cast, get_args, get_origin from typing import get_type_hints as typing_get_type_hints -from playwright._impl._accessibility import Accessibility from playwright._impl._assertions import ( APIResponseAssertions, LocatorAssertions, @@ -57,6 +56,7 @@ def process_type(value: Any, param: bool = False) -> str: value = str(value) + value = re.sub("pathlib._local.Path", "pathlib.Path", value) value = re.sub(r"", r"\1", value) value = re.sub(r"NoneType", "None", value) value = re.sub(r"playwright\._impl\._api_structures.([\w]+)", r"\1", value) @@ -224,7 +224,6 @@ def return_value(value: Any) -> List[str]: from typing import Literal -from playwright._impl._accessibility import Accessibility as AccessibilityImpl from playwright._impl._api_structures import Cookie, SetCookieParam, FloatRect, FilePayload, Geolocation, HttpCredentials, PdfMargins, Position, ProxySettings, ResourceTiming, SourceLocation, StorageState, ClientCertificate, ViewportSize, RemoteAddr, SecurityDetails, RequestSizes, NameValue, TracingGroupLocation from playwright._impl._browser import Browser as BrowserImpl from playwright._impl._browser_context import BrowserContext as BrowserContextImpl @@ -264,7 +263,6 @@ def return_value(value: Any) -> List[str]: Touchscreen, JSHandle, ElementHandle, - Accessibility, FileChooser, Frame, FrameLocator, diff --git a/setup.py b/setup.py index 7b32878dd..fa81e5e85 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ import zipfile from typing import Dict -driver_version = "1.51.1" +driver_version = "1.57.0-beta-1764944708000" base_wheel_bundles = [ { @@ -66,6 +66,12 @@ "platform": "win32", "zip_name": "win32_x64", }, + { + "wheel": "win_arm64.whl", + "machine": "arm64", + "platform": "win32", + "zip_name": "win32_arm64", + }, ] if len(sys.argv) == 2 and sys.argv[1] == "--list-wheels": @@ -93,9 +99,10 @@ def extractall(zip: zipfile.ZipFile, path: str) -> None: def download_driver(zip_name: str) -> None: zip_file = f"playwright-{driver_version}-{zip_name}.zip" - if os.path.exists("driver/" + zip_file): + destination_path = "driver/" + zip_file + if os.path.exists(destination_path): return - url = "https://playwright.azureedge.net/builds/driver/" + url = "https://cdn.playwright.dev/builds/driver/" if ( "-alpha" in driver_version or "-beta" in driver_version @@ -103,9 +110,11 @@ def download_driver(zip_name: str) -> None: ): url = url + "next/" url = url + zip_file + temp_destination_path = destination_path + ".tmp" print(f"Fetching {url}") # Don't replace this with urllib - Python won't have certificates to do SSL on all platforms. - subprocess.check_call(["curl", url, "-o", "driver/" + zip_file]) + subprocess.check_call(["curl", url, "-o", temp_destination_path]) + os.rename(temp_destination_path, destination_path) class PlaywrightBDistWheelCommand(BDistWheelCommand): diff --git a/tests/assets/client-certificates/README.md b/tests/assets/client-certificates/README.md index b0ee78e70..2c73bd92f 100644 --- a/tests/assets/client-certificates/README.md +++ b/tests/assets/client-certificates/README.md @@ -1,60 +1,7 @@ # Client Certificate test-certificates -## Server +Regenerate all certificates by running: -```bash -openssl req \ - -x509 \ - -newkey rsa:4096 \ - -keyout server/server_key.pem \ - -out server/server_cert.pem \ - -nodes \ - -days 365 \ - -subj "/CN=localhost/O=Client\ Certificate\ Demo" \ - -addext "subjectAltName=DNS:localhost,DNS:local.playwright" ``` - -## Trusted client-certificate (server signed/valid) - -``` -mkdir -p client/trusted -# generate server-signed (valid) certifcate -openssl req \ - -newkey rsa:4096 \ - -keyout client/trusted/key.pem \ - -out client/trusted/csr.pem \ - -nodes \ - -days 365 \ - -subj "/CN=Alice" - -# sign with server_cert.pem -openssl x509 \ - -req \ - -in client/trusted/csr.pem \ - -CA server/server_cert.pem \ - -CAkey server/server_key.pem \ - -out client/trusted/cert.pem \ - -set_serial 01 \ - -days 365 -``` - -## Self-signed certificate (invalid) - -``` -mkdir -p client/self-signed -openssl req \ - -newkey rsa:4096 \ - -keyout client/self-signed/key.pem \ - -out client/self-signed/csr.pem \ - -nodes \ - -days 365 \ - -subj "/CN=Bob" - -# sign with self-signed/key.pem -openssl x509 \ - -req \ - -in client/self-signed/csr.pem \ - -signkey client/self-signed/key.pem \ - -out client/self-signed/cert.pem \ - -days 365 +bash generate.sh ``` diff --git a/tests/assets/client-certificates/client/localhost/localhost.csr b/tests/assets/client-certificates/client/localhost/localhost.csr new file mode 100644 index 000000000..cbf60ba43 --- /dev/null +++ b/tests/assets/client-certificates/client/localhost/localhost.csr @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIEizCCAnMCAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAuxTIfOTWJzsLSYT/RXu6eUYe95oFA0gMM6pdQAws +U08ufU8S68UzBIAb6zdwDqM6LD5FqLrwaTsUW73dKefb003EgA19L3sZTHSwnSv9 +g9+rnKw/8EA+1thFo7h3EfdWkO/nuxnPfEnHu5oxMk/mCu+hdjfgh01mxc00PIO/ +wioxZtuhzKNIUu+qQAj70Dwbkzy3Zb1QAnj+02AUYnrVTYhusmP4pjrfYgAWU1x4 +WN+XTSI27LnQHE8t8OxuICFwqEEPcJg4pCFNQgmO0gcRDbd4U7JgQ0xMXZ9na9k0 +f1GOgtjKsz5IIDrAwLnrIOEXiIYlNaQ/v28BrlWytaOHGFKmDxAbGXHz1WuftmR1 +vYQivC5fkXU3+QKWoGPZaLIhA7/ZOMSVgoo+Rv+mS6Mze/6LphatUMcI1oYX4ZBY +FrsVsAeAO/FPZDcwVct0bdPCRRFBQpgl6DxMyItHMexNHwURi4YHwhYiBBe+Njc1 +tTh6dniA11MUVC5WmKME7CxgXxHVXE4MYcf/w1x4sOnxHneb1bCa8jOPELRc6byR +8mk23fbBP42EPkrgpCtLhbHgskLKobLFav43xaIiYFn744U7rbINI+5RfJ8BH5Kw +CB5RlAsymPxwDNcPXyyyeHEEUAQmtnJPrGPSGMN/QODgCWRXEvY556NGx0D5riu7 +MXkCAwEAAaAyMDAGCSqGSIb3DQEJDjEjMCEwHwYDVR0RBBgwFoIJbG9jYWxob3N0 +ggkxMjcuMC4wLjEwDQYJKoZIhvcNAQELBQADggIBAEYqpx28FFRHqXfyumTkkFb/ +hrlUD9qKUYSdjpsPLFRUYiLofNlYhCtEFEuHyLkNHqLO5Z6E7yOpm/HvdXY0eBQy +tK0verggKXy8MnpLVtsFOf4DgenTwFRG3NbNk2/KynwiAwJ0iwOdDfAmwrzYFLeR +uXysPE0lGJu277akuo/PDg3QEY44iDJgI4rqtY25sR8J23M9Roa5qN4bdaXl6l10 +UJhdugTG90gRFmmUVLLRa21A3j4m7pvWHu7zYJN7ylS/NQ6ZEnpNno7Ie6MLzrm6 +h8MOSoxtmrXi0e3aYf7PC3u3YLAjyUzHicgMdXk8SjtUXi6l3U5hU+6WBeOKKmwa +vi+FUpRfnutkWpinNqikmMw0Sqy+bpXwbwgm2oD6driiMneBCT37gW/IyUPWVxzg +dVgeQIt3i8SMwnGUrzfVo/gqjq4qFBpzS3h9jPPrMYQN2LQdmyo13R9UH26cLGE1 +cgVmqUKa11kX8353wfA36JuW5yGv2yaK4V1kwFISznMvk5AGdOHONdZOrN4sLIIY +7Npqp5AO1KdoYcNVptCA4n6mN/30fTmA/W1ajsKcfIn5RlmvdgpooKoEgaU/X9RL +d2ydIpijWbQqm+nF9skpO4jIFPfuQj6mMXCPWtsvbpHCoo0iMHHWfVFcpvsXGYN2 +6ji5z3opVapp1m5uTmcA +-----END CERTIFICATE REQUEST----- diff --git a/tests/assets/client-certificates/client/localhost/localhost.ext b/tests/assets/client-certificates/client/localhost/localhost.ext new file mode 100644 index 000000000..d4d022726 --- /dev/null +++ b/tests/assets/client-certificates/client/localhost/localhost.ext @@ -0,0 +1 @@ +subjectAltName=DNS:localhost,DNS:127.0.0.1 diff --git a/tests/assets/client-certificates/client/localhost/localhost.key b/tests/assets/client-certificates/client/localhost/localhost.key new file mode 100644 index 000000000..7f6adba53 --- /dev/null +++ b/tests/assets/client-certificates/client/localhost/localhost.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +DjCwHSmcSUmAmx7IXfoyRQuKLeRw +-----END PRIVATE KEY----- diff --git a/tests/assets/client-certificates/client/localhost/localhost.pem b/tests/assets/client-certificates/client/localhost/localhost.pem new file mode 100644 index 000000000..5c1a67505 --- /dev/null +++ b/tests/assets/client-certificates/client/localhost/localhost.pem @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFKDCCAxCgAwIBAgIBATANBgkqhkiG9w0BAQsFADA2MRIwEAYDVQQDDAlsb2Nh +bGhvc3QxIDAeBgNVBAoMF0NsaWVudCBDZXJ0aWZpY2F0ZSBEZW1vMB4XDTI1MDcy +MTE1NTYxOVoXDTM1MDcxOTE1NTYxOVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIC +IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuxTIfOTWJzsLSYT/RXu6eUYe +95oFA0gMM6pdQAwsU08ufU8S68UzBIAb6zdwDqM6LD5FqLrwaTsUW73dKefb003E +gA19L3sZTHSwnSv9g9+rnKw/8EA+1thFo7h3EfdWkO/nuxnPfEnHu5oxMk/mCu+h +djfgh01mxc00PIO/wioxZtuhzKNIUu+qQAj70Dwbkzy3Zb1QAnj+02AUYnrVTYhu +smP4pjrfYgAWU1x4WN+XTSI27LnQHE8t8OxuICFwqEEPcJg4pCFNQgmO0gcRDbd4 +U7JgQ0xMXZ9na9k0f1GOgtjKsz5IIDrAwLnrIOEXiIYlNaQ/v28BrlWytaOHGFKm +DxAbGXHz1WuftmR1vYQivC5fkXU3+QKWoGPZaLIhA7/ZOMSVgoo+Rv+mS6Mze/6L +phatUMcI1oYX4ZBYFrsVsAeAO/FPZDcwVct0bdPCRRFBQpgl6DxMyItHMexNHwUR +i4YHwhYiBBe+Njc1tTh6dniA11MUVC5WmKME7CxgXxHVXE4MYcf/w1x4sOnxHneb +1bCa8jOPELRc6byR8mk23fbBP42EPkrgpCtLhbHgskLKobLFav43xaIiYFn744U7 +rbINI+5RfJ8BH5KwCB5RlAsymPxwDNcPXyyyeHEEUAQmtnJPrGPSGMN/QODgCWRX +EvY556NGx0D5riu7MXkCAwEAAaNjMGEwHwYDVR0RBBgwFoIJbG9jYWxob3N0ggkx +MjcuMC4wLjEwHQYDVR0OBBYEFIE6kbIR0PlzaJuZg52JqXuFFQHBMB8GA1UdIwQY +MBaAFPHaEfjJoIftSkHTb8mwme27LtifMA0GCSqGSIb3DQEBCwUAA4ICAQB7E57H +19qUD4DjLxJGAVpDphD8mZg9aR7bUd6laXZ12VQnn3+OrF+6JjJ2TIr8ssRkkzc6 +rMhRqob+DxeB94JqlFQDmwP37wJXnuTtuGj71NHQal15b5ZB28fFdwWgUlYECWyg +sYeMK5HMnNDziniRnjPoKU6f8urmUW2G8N2SOufnRar/StY/8u24IFh/vDTVJKWB +a22pVk/PIx6JcErfMx0OJdoIUZd/C9DT/a7t+Pt+t3EPVNM2fcjs9aMcg6tlU6Tp +YwHMzkFQYmcrp2QEhwy23z2E+wnLbeHTULDsHVOLudUGDcSgkW48bPNTOtZTysQ+ +P4mJf+Ju2HC/EwuVrOa14uuay9c+hC1Q1gsGbLZB8NE6jOX5QYN2a7iiUEdCdufo +wWIXEC+2dJiEji3ehwJp0q4jK15X0IOIfxLH4PholORH659DdCfpm7YAjoaPE9nr +28MMd9Lj6zWmfyeb5p0IUDpkreL2huxjXHJkmSm4dcxDxLqsP6KSPlhrPvjAVUqw +5M/jUarDAHaPLNlwqcqWODb891FIyRmc6YHtSRGujMyfJ8LRS2f9rHeyM3m2jMHf +8EahOgyQ8nvSs/a42VS97IHkGl6qufK7ZUq/VMhQRb+KbaIDN5TLtNwEQ1I40Bm9 +UOoSX/uP1PstOZvf9gnYto8sf75B+PxO692uYw== +-----END CERTIFICATE----- diff --git a/tests/assets/client-certificates/client/self-signed/cert.pem b/tests/assets/client-certificates/client/self-signed/cert.pem index 3c0771794..d14ac95b2 100644 --- a/tests/assets/client-certificates/client/self-signed/cert.pem +++ b/tests/assets/client-certificates/client/self-signed/cert.pem @@ -1,28 +1,28 @@ -----BEGIN CERTIFICATE----- -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 -----END CERTIFICATE----- diff --git a/tests/assets/client-certificates/client/self-signed/csr.pem b/tests/assets/client-certificates/client/self-signed/csr.pem index 4c99e1349..283dd45a6 100644 --- a/tests/assets/client-certificates/client/self-signed/csr.pem +++ b/tests/assets/client-certificates/client/self-signed/csr.pem @@ -1,26 +1,26 @@ -----BEGIN CERTIFICATE REQUEST----- MIIEUzCCAjsCAQAwDjEMMAoGA1UEAwwDQm9iMIICIjANBgkqhkiG9w0BAQEFAAOC -Ag8AMIICCgKCAgEA179eTsqcc1c3AOQHzCZEyYLPta2CCAscUFqcEZ9vWvjW0uzO -v9TDlB33Unovjch4CElZOBhzTadVsbmnYKpxwyVU89WCuQKvedz4k1vu7S1YryfN -bmS8PWbnQ4ds9NB7SgJNHZILvx9DXuWeFEyzRIo1984z4HheBzrkf791LqpYKaKz -iANUo8h8t0dmTX/boOz8cEnQNwtTC0ZX3aD0obG/UAhr/22ZGPo/E659fh4ptyYX -2LrIUHGy+EuxnJ9Y4cTqa88Ee6K6AkDiT/AoNQNxE4X++jqLuie8j/ZYpI1Oll38 -GwKVOyy1msRLtoGmISNwkMIQDGABrJlxgpP4QQAQ+08v9srzXOlkdxdr7OCP81r+ -ccBXiSQEe7BAkdJ8l98l5dprJ++GJ+SZcV4+/iGR0dKU2IdAG5HiKZIFn6ch9Ux+ -UMqeGaYCpkHrTiietHwcXgtVBlE0jFmB/HspmI/O0abK+grMmueaH7XtTI8YHnw0 -mUpL8+yp7mfA7zFusgFgyiBPXeD/NQgg8vja67k++d1VGoXm2xr+5WPQCSbgQoMk -kOBMLHWJTefd6F4Z5M+oI0VwYbf6eQW246wJgpCHSPR0Vdijd6MAGRWKUuLfDsA9 -+12iGbKvwJ2enJlStft2V2LZcjBfdIMbigW1aSVNN5w6m6YVrQPry3WPkWcCAwEA -AaAAMA0GCSqGSIb3DQEBCwUAA4ICAQCb07d2IjUy1PeHCj/2k/z9FrZSo6K3c8y6 -b/u/MZ0AXPKLPDSo7UYpOJ8Z2cBiJ8jQapjTSEL8POUYqcvCmP55R6u68KmvINHo -+Ly7pP+xPrbA4Q0WmPnz37hQn+I1he0GuEQyjZZqUln9zwp67TsWNKxKtCH+1j8M -Ltzx6kuHCdPtDUtv291yhVRqvbjiDs+gzdQYNJtAkUbHwHFxu8oZhg8QZGyXYMN8 -TGoQ1LTezFZXJtX69K7WnrDGrjsgB6EMvwkqAFSYNH0LFvI0xo13OOgXr9mrwohA -76uZtjXL9B15EqrMce6mdUZi46QJuQ2avTi57Lz+fqvsBYdQO89VcFSmqu2nfspN -QZDrooyjHrlls8MpoBd8fde9oT4uA4/d9SJtuHUnjgGN7Qr7eTruWXL8wVMwFnvL -igWE4detO9y2gpRLq6uEqzWYMGtN9PXJCGU8C8m9E2EBUKMrT/bpNbboatLcgRrW -acj0BRVqoVzk1sRq7Sa6ejywqgARvIhTehg6DqdMdcENCPQ7rxDRu5PSDM8/mwIj -0KYl8d2PlECB4ofRyLcy17BZzjP6hSnkGzcFk0/bChZOSIRnwvKbvfXnB45hhPk8 -XwT/6UNSwC2STP3gtOmLqrWj+OE0gy0AkDMvP3UnQVGMUvgfYg+N4ROCVtlqzxe9 -W65c05Mm1g== +Ag8AMIICCgKCAgEAzQMXYOZz3ILrbF9qpDng2pw2wJf1UFopehwaYyu7riWJ9+AD +rhSnCSFBM3ScMBc/8dIR6etWwci8QwJ/MtvIU0yx4llq+53G+19Bc1teC6q/b4QC +RDIcTGxOZoR+jfYZVjPODEyJ5y5MZIo34ZP4bu+JnpT4W7+uojm3jOoyNPqXMcc7 +0uAhfSKG+JfrwZKteA4T5VKFytVcWgh4v03z8zTYeW3kD4lCsongBz6yu2dn3D6X +MROnxiPwi+IRQqX1pwnJ0UA3CTeOHEw1jA3koxWIIg44PWaPaCj9Udrhf4ew00XL +PWVZP8T5rVf8yUfecWQCR7FueJFqoLhPMMFi17rYmGZUvw3/YkXBjay4Q9e+G2WS +3Xk8u+I1sCuVBJNBRv9DqtMC9D/N9NI8GkLrXwZmk82SXG+cQ0TSkNUHYI/03YKo +qsn5H8PsG7Tc+Y2Ca6TaCWims7lvOg7U0E6lu2h5NGcdWHFPJ9qfe+xho/yfYYwG +qEKanGAu1kd5SbIaX6/YiM5/Pp/96MeRNrB9kLzDnZTNuGtdCawVFgbkWmfX2Z6/ +a0d6SvZGDzBxxTVZRB0my01E1FP53MS8YRH98HUjGEwNRVq2e1W+aXldKppgZ85G +ZD3l2YTeuI0iPJCDUzQiaWzFtWc8s13YQ1HLCOyOXF7QqMyNCiLGb4xQ56kCAwEA +AaAAMA0GCSqGSIb3DQEBCwUAA4ICAQAn/ZI7IkBUEfhZHefwtF+QHCyxSEKvqwHq +fSqKVdarBPz8Ik8m3icj8R/DcS3y5jgzx3x8bXQoDpgsAQgeb825NRv2wAQAGoH1 +8vh204lTyjqzrgtK7eQeQDc7fjeigIkxQsAK9zk4BaFUWp0wEC0RLVAgvlQTl7vu +n1jSSrhK8tvGy62/cIxZfwD0bAMHlW4m1A4fUuSGWQX2KldgA8tnmT6wx0If/nKb +VB68AMbyMHUeb32v9wEvx2nHlwMjqNFeg7vYyJXOfBdDILUl+OTBoQY1X+jSx5iM +txTzmA8Hcgx0Fq+BnbQuZCLqFpNWEfenAtQtaAFuJwMiKCf6kgbqkDVShJkmt+vC +j3dcsVMZDsdMk4qRpiJhaTQOYmsMGCj4uoDpFGjwPoUwlDkjYgHAAsm9uCkshc+m +WZO7I6Z3Tbi3XskJvAMc3dTWjtc6nApEtr/mn8LcETfOp7RRSfjllj6ijWUrVwUy +BpzU9C/zLTkhFX0DVDCIV+jEefF8JPfzSKLgXyRbInTz1/6/sKXtswXW0NjzqLMI +C9ggMBhOiDv9KJn3G/mY4CqIfo9KMzF+++4t+wdXTir8DWNlMUAn1vlBwxZAgKCM +GonVExBU0VIGCpyTRLkesEHnPMgybP6gLzP3++54x288OS5JwuPPtkDcsBHUjTq8 +HxTJvUul/Q== -----END CERTIFICATE REQUEST----- diff --git a/tests/assets/client-certificates/client/self-signed/key.pem b/tests/assets/client-certificates/client/self-signed/key.pem index 70d5e3dd0..c3ddd1a91 100644 --- a/tests/assets/client-certificates/client/self-signed/key.pem +++ b/tests/assets/client-certificates/client/self-signed/key.pem @@ -1,52 +1,52 @@ -----BEGIN PRIVATE KEY----- -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 -dg6m3MAyZ1IXjHlogOdmGvnq07/w +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 +v3/WVVAXEcIowL3T4jzJ0RLdf1qE8Bc= -----END PRIVATE KEY----- diff --git a/tests/assets/client-certificates/client/trusted/cert-legacy.pfx b/tests/assets/client-certificates/client/trusted/cert-legacy.pfx new file mode 100644 index 000000000..9f06aa35c Binary files /dev/null and b/tests/assets/client-certificates/client/trusted/cert-legacy.pfx differ diff --git a/tests/assets/client-certificates/client/trusted/cert.pem b/tests/assets/client-certificates/client/trusted/cert.pem index 76d1e1a54..3430d063c 100644 --- a/tests/assets/client-certificates/client/trusted/cert.pem +++ b/tests/assets/client-certificates/client/trusted/cert.pem @@ -1,29 +1,29 @@ -----BEGIN CERTIFICATE----- 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 -----END CERTIFICATE----- diff --git a/tests/assets/client-certificates/client/trusted/cert.pfx b/tests/assets/client-certificates/client/trusted/cert.pfx new file mode 100644 index 000000000..391dea7b8 Binary files /dev/null and b/tests/assets/client-certificates/client/trusted/cert.pfx differ diff --git a/tests/assets/client-certificates/client/trusted/csr.pem b/tests/assets/client-certificates/client/trusted/csr.pem index 8ead6da3d..fc930ea7c 100644 --- a/tests/assets/client-certificates/client/trusted/csr.pem +++ b/tests/assets/client-certificates/client/trusted/csr.pem @@ -1,26 +1,26 @@ -----BEGIN CERTIFICATE REQUEST----- MIIEVTCCAj0CAQAwEDEOMAwGA1UEAwwFQWxpY2UwggIiMA0GCSqGSIb3DQEBAQUA -A4ICDwAwggIKAoICAQCac3+4rNmH4/N1s4HqR2X168tgS/aA6sHW5at8mWRnq54N -m11RvnK55jHQYVAdBgJy5M07w0wakp8inxzlY95wqxBimYG63Un/1p7mX9FkB4LN -ISCc6j/s/Ufv85MXPbn0S5rm9UcQO9cINJb1RP1YgDDLN5cxMz6X4nyofN8H6Lhv -h4JDdBw4DfDEFERkVfF+bkZ7YW4XHEChgzm3RxCF0eeGzIXGrkkK9AsSdJAhOvTl -HPFCQKXTYZhsL5+3Ma4RnWnDWvLTHx6KzoU+twTM2mYhhQuQgQpnmDHxGge8kGeH -GtfdgAjtVJTE57xF/shP0JU+tuIV8NNhQ/vEmhL0Wa093/EvpTVp0EUEuDh9ORRH -5K5M4bKJyU4XX5noiht6yOn00uaoJcWduUAWsU+cDSvDTMw81opWWm0QIAV3G2yu -RSkumHAKqvQLeyeyiKz+OEhyEiZ7EZNExPD0TSpApSTU6aCTUAvPYGQ59VjsMHTu -J9r4wKIYaDvfL+t72vg2vTQma5cTOBJfIdxH9blFTjEnToH3LX8t0XndQ2RkiRnI -ze2p2jUShxo/lWCjCw+2Iaw0A0fNUK1BbOrFRPq1u7AnEuMJt7HF50MloItM97R9 -vofDwgDIzlX/PzlVRcn1WCo8Fr/0EXxPPreX0YDIp1ANQ8fSv7bKb2vQIxWuCQID -AQABoAAwDQYJKoZIhvcNAQELBQADggIBAGgf3EC8WL3RGmuGA+d/4wd1jNfrfU6n -xjnDwdEEX0TQZGGPjh5xvoCK76yZPkO6+z0IYSepEmWBS27HJKl7nuoOvS7MjQyJ -C+3Bdk3ToCeQjmNBlRBKsUw5ftTU902oMl5BptHGj1KGjYBLAkPdXb44wXSVKJ8q -ihFhWlovsva6GDoUorksU3vOwijdlGzTANQHJGFncgrRud9ATavpGS3KVxR73R3A -aBbu3Qw+QIfu8Qx5eBJp8CbMrpAmjfuq17STvqr5bC10Fnn4NegrnHOQG9JcK02+ -5Bn3+9X/n1mue7aohIdErLEiDMSqMOwFfrJeaH6YM1G4QkWyqGugtmHsWOUf0nlU -nkH1krvfw9rb6b+03c4A6GSeHnbX5ufFDSf5gaR6Wy7c0jBnoxVbtBLH2zXlrd0k -iRQG7C6XZzGMS7hb7GL7+bkRy9kWjmDL7z7Fp+EgzKhNmzuWII3E9X9va33HoQ/Q -UdK3JVToxRQg6XRKOxL9+U/+8i6U8lxObLWkWh2cypZqbz5qJxa+2u5JYO/KEoHZ -G963UX7XWezR98vZuTc1XHGZtBDMrjjDd7Kmb4/i/xBPeWwseeGtzFy9z2pnEnkL -uKE4C8wUNpzUUlsn4LneZXObIoErE7FqAAlVFujVe7iaJBmXoUXZR36drbfiaODK -vwAGyrYHaOlR +A4ICDwAwggIKAoICAQC4ggn74iAHLgOWOiOvB2CPe+Hr7W6STYJLZOoqPdh7mv7Q +Gm8cYxfD+26p13aEaW4/qn45losWdEPPy2ZiVIF+kcOP0R4AqsB0w9UHT4WSzCWt +Dqs8ywDMJ6tHge0++8S1bTpdutn/m8DPnKtkD9RQUzLFmGDO+mB08Xu+egTzJvUR +bHXRJ27E+CXUXLEHbAJd8EKjJiQYXhcj4lzUXOUg+xkpPAGe1dgQ6BDkv3xq/81S +9NTIT5YriHEm6egi9AFJLZbbZtCpRQm1MDMq7zfD7oL6ECLGrJ/aaxLEM49gMdw2 +SscYHotVxX2OMAKgN/ytB18L/mIQ4pOWp3HJQ+nks9Zu1V8cg7Pz5pWwjqoncbyU +aBi3vGsitAot3cyLXbN9hH8zQB8QqGyNLvqQnCJOBlYxOPA8M8NQGDThWKspcix0 +LhkmnYsWWt3+KEGEpFRcJCEthm/DAd1GImP7/dg1/btT/zOsIIkIoCyBwznuR4M4 +XoNTbIBsiTnO/6PjOdTrjLVX7vMueTjdbCgK3VeOuwetbZ7aZ/hvMJCjJ4wuM3l2 +ZyfEQlP4gJLJS3Fw2PRsySZJl9JyEzwTqYVosDF5SF8uw0HIcFaliZCqBbgYfOiQ +zjIX/GiHZdor1ZrhAbO/MpSxuxGzxF7Em4n+4p816NnC9S2JbHRetkM9P1adHwID +AQABoAAwDQYJKoZIhvcNAQELBQADggIBACpScaAoLAs9DuTcI5Y4dbHf7LwF4Zxx +UgPNzE1HB1Pr6NaHRiOWrlCtDEB5UwHrr0oZuRTqSEGg3Pe5Z2QtPG/sdFgfm4BP +29o6qo0CXiEVBwU7/K0/lL2/0a7uSbD0Tkw9d6Bgik7/Z5rzXZIi2vtUcrOtHskP +C+Z9w3vH9a+RnUeo52mCnRi4SaiSEnD5jvhmgaI9iF+k5pYBiMrKRj5W/F1QCkf4 +7OuSK97xN0eG/I4Oxgzi/qt51ySCYZbqoh7dIpwi/a4UsK8kdzDDI1M3J7bU07cO +CJRfr0EETqCQw/gAKoag3tRFNvWQB6Z9G3Ev5jeaCLpcc32NlpN5xH3VW8A0Zb81 +dn5BXkPSxjwJaD0a3cLFkfgrasoe7ZMmHrVpQDw+9USuGCYXMPzNZLEeTFbrRkUn +sqi30e28E1H69zVWj+OKzCWEH/azVlfaoVbwM+njUJDe5V09KvFtI7aZYmvLxbUX +4ifoRUVoKedyKnueVmoIG57lF2VzeEhX5YjCngxIg+YuE99HkMQAZSlS6uJcVM92 +tsC/+pYECBk8ukenbxmKXROl3u4p2M1iCSL/8EOVROuyjnuzCXJZOpNptdpX4ZgL +kHP1erq7/U8ZU8HviUsfMoisagx8dA8uj/4fk0jfNxOJqlZL9eJhpgBfYHqmAz3h +m+PQVw96eeoK -----END CERTIFICATE REQUEST----- diff --git a/tests/assets/client-certificates/client/trusted/key.pem b/tests/assets/client-certificates/client/trusted/key.pem index d60201e5a..a11cdc8a7 100644 --- a/tests/assets/client-certificates/client/trusted/key.pem +++ b/tests/assets/client-certificates/client/trusted/key.pem @@ -1,52 +1,52 @@ -----BEGIN PRIVATE KEY----- -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 -Y+eu85cike5J4vnz8uG04yt7rXjIrUav +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 +21eLmxOuln98Q3na7ftpAWQdyjcE6Q== -----END PRIVATE KEY----- diff --git a/tests/assets/client-certificates/generate.sh b/tests/assets/client-certificates/generate.sh new file mode 100755 index 000000000..f835f5f32 --- /dev/null +++ b/tests/assets/client-certificates/generate.sh @@ -0,0 +1,88 @@ +#!/bin/bash + +# Client Certificate test-certificates + +cd "$(dirname "$0")" + +## Server + +openssl req \ + -x509 \ + -newkey rsa:4096 \ + -keyout server/server_key.pem \ + -out server/server_cert.pem \ + -nodes \ + -days 3650 \ + -subj "/CN=localhost/O=Client\ Certificate\ Demo" \ + -addext "subjectAltName=DNS:localhost,DNS:local.playwright" + +## Trusted client-certificate (server signed/valid) + +mkdir -p client/trusted +# generate server-signed (valid) certificate +openssl req \ + -newkey rsa:4096 \ + -keyout client/trusted/key.pem \ + -out client/trusted/csr.pem \ + -nodes \ + -days 3650 \ + -subj "/CN=Alice" + +# sign with server_cert.pem +openssl x509 \ + -req \ + -in client/trusted/csr.pem \ + -CA server/server_cert.pem \ + -CAkey server/server_key.pem \ + -out client/trusted/cert.pem \ + -set_serial 01 \ + -days 3650 +# create pfx +openssl pkcs12 -export -out client/trusted/cert.pfx -inkey client/trusted/key.pem -in client/trusted/cert.pem -passout pass:secure + +## Trusted certificate for localhost (server signed/valid) + +mkdir -p client/localhost + +# generate server-signed (valid) certificate +openssl req \ + -newkey rsa:4096 \ + -keyout client/localhost/localhost.key \ + -out client/localhost/localhost.csr \ + -nodes \ + -days 3650 \ + -subj "/CN=localhost" \ + -addext "subjectAltName=DNS:localhost,DNS:127.0.0.1" + +# put extensions +echo "subjectAltName=DNS:localhost,DNS:127.0.0.1" > client/localhost/localhost.ext + +# sign with server_cert.pem +openssl x509 \ + -req \ + -in client/localhost/localhost.csr \ + -CA server/server_cert.pem \ + -CAkey server/server_key.pem \ + -set_serial 01 \ + -out client/localhost/localhost.pem \ + -days 3650 \ + -extfile client/localhost/localhost.ext + +## Self-signed certificate (invalid) + +mkdir -p client/self-signed +openssl req \ + -newkey rsa:4096 \ + -keyout client/self-signed/key.pem \ + -out client/self-signed/csr.pem \ + -nodes \ + -days 3650 \ + -subj "/CN=Bob" + +# sign with self-signed/key.pem +openssl x509 \ + -req \ + -in client/self-signed/csr.pem \ + -signkey client/self-signed/key.pem \ + -out client/self-signed/cert.pem \ + -days 3650 diff --git a/tests/assets/client-certificates/server/server_cert.pem b/tests/assets/client-certificates/server/server_cert.pem index 52d8f5314..18ee3d805 100644 --- a/tests/assets/client-certificates/server/server_cert.pem +++ b/tests/assets/client-certificates/server/server_cert.pem @@ -1,32 +1,32 @@ -----BEGIN CERTIFICATE----- -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 -2Ct10prMWB/3 +cmlnaHQwDQYJKoZIhvcNAQELBQADggIBAH1/6Sp4dW96yvwi9ptgVRYfRSRWfYYy +2nU6kJ1DPOW7hTPf2wLf6Z2KqiJXn8tECHfM4pnPSgDhtZHDDHAu9l7Diqzk/NIl +AblRs++4vSKdnCx1uh3EFLjIMmHa/cRUzfuR+oGfri3v7jgTBV6UJJQ0pMqIDk/P +VEOWWukllru40M8Rwy4F6LmpwIWMtNDxmhtXSbQJ6TZn+isNSoaWgHjQmayrVyXZ +OcNYuR8M/ECvnufuIOW53+hhwG1X0jEoBdXqqXbTKcGjA+yLHp008AAB5DNQblYm +FJ4N0v6eYLhOO0u3n0b/ZsqUkZx2h38tlZoAdJNqy+d4TE3WbGRNgNJrTjAOzpPL +cQ5RNr3dDsUDFPnCoK3LUf9BWerARoDt+bbrM/VqvWH/0uqaU0/vo8IchSyQ9wGv +SwrWLJU32HQUJ2VQP/m4ADf3X2Ozj+e8bBf7XPPD8KDyL4aR0KuYzrsOnhFlBkHD +PtNjc7SgiE7AZe1WOEmhBQcr+vI6S8qFTurTntCBkvrFgYBAIIhK2XwFN5sFtByr +GCB67NQMp37g2qFNYi8EmKQvPLpAcC3Je5PvHFSj2y9Z14FclGoxjU50SlH9/A5I +iHGcIVjCv9NhNDFAwakF/sTnrpT/FIS7tm9GwoJGWCdYg4pl72WYCW6hJBG+XY7G +t3jt9TP8623D -----END CERTIFICATE----- diff --git a/tests/assets/client-certificates/server/server_key.pem b/tests/assets/client-certificates/server/server_key.pem index ff6a3fc11..3a762bde7 100644 --- a/tests/assets/client-certificates/server/server_key.pem +++ b/tests/assets/client-certificates/server/server_key.pem @@ -1,52 +1,52 @@ -----BEGIN PRIVATE KEY----- -MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQC+K5JWhlfvI47Z -L/AzL0xnOl+cMelr2BqH+7XS8187SbvluhFfFkq/7V7rwgsHI64sn8pgRCOnqKWV -6jtb651dGzn7Nby6InmyOQzF4VwfSVWQ6BYXgXuryS9Gm0gi8sOL1Ji/jV49n1gz -LyIxLNhd7NG2DCCedTHJnxyz4xq8MWhI/qI85iWJqcHhxkDb8wtH1Vd6nd/ZRVDb -jgTvPH3EDK7JqmnYG9+x4Jz0yEhvV7jL3gNu2mIyttvm7oRna9oHgaKFUJt4BCfP -bT5U3ipvcq29hdD5/5QIDzTWcExTnklolg5xpFext1+3KPSppESxcfBBNoL3h1B8 -ZcZalEMC/IoFUIDJQj5gmSn4okwMWIxgf+AL0609MKEqQ2FavOsvBmhHcQsqLk4M -O/v0NGFv1/xGe4tUkX4han6ykf1+sqzupJT5qnUONmvghb2SpIt83o4j4KHVzZwk -8JK0N6hN7JEjXQwSKCh3b0FFg+kPAe12d6BBcsNzEYmt2C1KNPbXMX84zIkgPN01 -XMg6kdCdjP6DH7CK+brW9qQufOqYpd3eNhJyeBm+oP3PhnhEiMTIO8X2GdSN5Rxo -zgxlVIj/QWhLV64r5AqPr/Vpd1vcsxrg3aS5CASmoWQmTPuhEZptRtrkPkGw7k9N -PZ34lnRenvKJ9e3DXhXRMqeYUY6wjwIDAQABAoICABfDfxpj2EowUdHvDR+AShZe -M4Njs00AKLSUbjCpq91PRfUbjr8onHemVGW2jkU6nrHB1/q2mRQC3YpBxmAirbvs -Qo8TNH24ACgWu/NgSXA5bEFa1yPh0M/zKH60uctwNaJcEyhgpIWjy1Q+EBJADduS -09PhaRQUBgAxa1dJSlZ5ABSbCS/9/HPa7Djn2sQBd4fm73MJlmbipAuDkDdLAlZE -1XSq4GYaeZYTQNnPy0lql1OWbyxjisDWm90cMhxwXELy3pm1LHBPaKAhgRf+2SOr -G23i8m3DE778E3i2eLs8POUeVzi5NiIljYboTcaDGfhoigLEKpJ+7L5Ww3YfL85Q -xk00Y0b+cYNrlJ3vCpflDXJunZ1gJHLDTixJeVMpXnMSi01+bSb8D/PTcbG3fZ0U -y4f2G0M+gf+m3EMMD96yerPf6jhGlTqY+eMyNVwNVk4BIG+D/8nf13keAF4kVbPJ -QMidnCNbu8ZiC12HqLyv3YZlseXPIkhpbYEhsj58sbG4Tms+mG/zPlTZjroIEdAX -nwI1aoG+NAbe+WSH/P4SvIMi1o/fWoXBtb+t7uy1AG/Xbu414WED7iwvxtqJRQj5 -rhrqryWTGQKY1zVJIOxwZP0f5gSIkEITyE+rO6o6pbAZFX7N0aMIvksBkEN5mdoV -RWzxfSVNGMWooRD5d3TZAoIBAQD1dvgOsLYP8lUfkKglLTqHQe3x75BVDR9zdTIt -tQh9UIbyovPFdLcXrHHJMBVMPTRGeRNpjCT5BNSNbidrmAxYN7YXuSA4uy3bubNU -76km5kmL2Ji+5u+qMm9Xycyqn30rLH9hT+9c/MVuPW6CNmETKX9+v9zb1v//RrBS -2ZNAWjJcBYv/rS/vKsW9yH/DbM21eSeokUqpkejOk1UxVZEcb9vt8VF8p+jO1wv3 -+UgI4Gfkf3sjEL1m/hBvH5Z49RHTFj4npeK6Lko4NLLazU2904jbHxppH51UNH1j -xp8Is+iNwW2qCOve8kSUUUjxLn4n45D2d+5qOqQTtsMWXHanAoIBAQDGVQ6UZqvo -djfcULq0Jub1xpBfxIAg7jSY7aZ6H0YlG7KgpVTd2TUEEKgErxtfYufjtLjjWb/d -lMG7UpkM5B4tFnpRDmvevltCqGsM3qi3AtPnzavgz2TAQy7qd2gJc8glE965LOfb -l+mGzE4SzeFJ9WS7sUDf4WnX2xjt3OA0VCvcBRNIwCnEvXu81XLKZL6etBx6zdCt -whWHIiqa4wkjuWEwvbeH4aWsh8gFY3E5mbvDdMFtyGWvTK8OGivl3CkdQxM+MOJD -3aAEBTr0M7tSMy5IKewASlAWZEVpFFPIyiyMCTI0XcEgA7ewHw/F3c7cstgVktjm -OYZytZPF0ZvZAoIBAB5+z0aT8ap9gtHPGPS1b8YKDNO33YiTfsrLTpabHRjkfj96 -uypW28BXLjO+g4bbO7ldpWnBfX5qeTWw77jQRQhYs4iy+SvTJVlc8siklbE9fvme -ySs+aZwNdAPGEGVKNzS77H9cfPJifOy7ORV4SAsnZq2KjJfLWDaQw6snWMHv8r23 -+rKjA4eFGtf/JtBSniPjj2fD1TDH7dJsP3NHnCWaSAqBpowEGEpKMTR3hdmEd6PN -qrCqjb1T5xrHI9yXJcXBx6sJUueqhJIDCg1g4D2rIB+I97EDunoRo1pX/L4KC+RA -ma08OoGSO67pglRkYEv4W7QjJj2QV34TgJ0wk5UCggEALINom0wT5z+pN+xyiv50 -NdNUEfpzW3C7I1urUpt0Td3SkJWq34Phj0EBxNNcTGNRclzcZkJ9eojpllZqfWcx -kqMJ3ulisoJ8zxAnvqK2sSSUVOFnYzSJA1HQ1NTp570xvYihI2R9wV5uDlAKcdP9 -bXEDI9Ebo2PfMpA9Hx3EwFnn4iDNfDWM6lgwzmgFtIE5+zqnbbSF0onN9R9o+oxc -P8Val+rspzWwznFHJlZ0Uh478xlgVHh2wgpu+7ZKBfQM0kF8ryefkOXMBTr7SVXX -BBLyn0Wxbzs+kFf+8B+c0mL17pQdzX0BXGMZNhEypBEtXYFSWD02Ky3cDCDOwsZR -uQKCAQAKQtsUSO80N/kzsWuSxHhuLMTvNZfiE/qK1Mz5Rw1qXxMXfYNFZbU/MqW7 -5DLd4Kn7s3v1UlBn2tbLGLzghnHYRxT9kxF7ZnY6HZv2IrEUjE2I2YTTCQr/Q7Z5 -gRBQb5z+vJbKOYnlSHurTexKmuTjgJ/y/jRQiQABccVj1w5lIm1SPoxpdKzSFyWt -0NVmff9VetoiWKJYldPBTOmqPUytuBZyX5fJ4pPixwgAns6ZaqJtVNyMZkZ/GoDk -XP2CvB/HyMiS7vXK5QJYYumk7oyC15H6eDChITNPV3VGH2QqcdEvDLT81W+JZ2mX -8ynLaTs3oV3BjQya9pAUyzIX5L67 +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 +dvQgA6dBjtYg5cQMHNbnfT3j3WzyQg== -----END PRIVATE KEY----- diff --git a/tests/assets/simple-extension/content-script.js b/tests/assets/extension-mv3-simple/content-script.js similarity index 100% rename from tests/assets/simple-extension/content-script.js rename to tests/assets/extension-mv3-simple/content-script.js diff --git a/tests/assets/simple-extension/index.js b/tests/assets/extension-mv3-simple/index.js similarity index 63% rename from tests/assets/simple-extension/index.js rename to tests/assets/extension-mv3-simple/index.js index a0bb3f4ea..1523a8364 100644 --- a/tests/assets/simple-extension/index.js +++ b/tests/assets/extension-mv3-simple/index.js @@ -1,2 +1,2 @@ // Mock script for background extension -window.MAGIC = 42; +globalThis.MAGIC = 42; diff --git a/tests/assets/simple-extension/manifest.json b/tests/assets/extension-mv3-simple/manifest.json similarity index 80% rename from tests/assets/simple-extension/manifest.json rename to tests/assets/extension-mv3-simple/manifest.json index da2cd082e..39b77fc21 100644 --- a/tests/assets/simple-extension/manifest.json +++ b/tests/assets/extension-mv3-simple/manifest.json @@ -2,7 +2,7 @@ "name": "Simple extension", "version": "0.1", "background": { - "scripts": ["index.js"] + "service_worker": "index.js" }, "content_scripts": [{ "matches": [""], @@ -10,5 +10,5 @@ "js": ["content-script.js"] }], "permissions": ["background", "activeTab"], - "manifest_version": 2 + "manifest_version": 3 } diff --git a/tests/assets/extension-mv3-with-logging/background.js b/tests/assets/extension-mv3-with-logging/background.js new file mode 100644 index 000000000..3b1a406fb --- /dev/null +++ b/tests/assets/extension-mv3-with-logging/background.js @@ -0,0 +1,5 @@ +console.log("Service worker script loaded"); + +chrome.runtime.onInstalled.addListener(() => { + console.log("Extension installed"); +}); diff --git a/tests/assets/extension-mv3-with-logging/content.js b/tests/assets/extension-mv3-with-logging/content.js new file mode 100644 index 000000000..e718206c2 --- /dev/null +++ b/tests/assets/extension-mv3-with-logging/content.js @@ -0,0 +1 @@ +console.log("Test console log from a third-party execution context"); diff --git a/tests/assets/extension-mv3-with-logging/manifest.json b/tests/assets/extension-mv3-with-logging/manifest.json new file mode 100644 index 000000000..5ad1fde38 --- /dev/null +++ b/tests/assets/extension-mv3-with-logging/manifest.json @@ -0,0 +1,17 @@ +{ + "manifest_version": 3, + "name": "Console Log Extension", + "version": "1.0", + "background": { + "service_worker": "background.js" + }, + "permissions": [ + "tabs" + ], + "content_scripts": [ + { + "matches": [""], + "js": ["content.js"] + } + ] + } diff --git a/tests/async/conftest.py b/tests/async/conftest.py index 65a963507..5dff9794f 100644 --- a/tests/async/conftest.py +++ b/tests/async/conftest.py @@ -13,19 +13,25 @@ # limitations under the License. import asyncio +from contextlib import asynccontextmanager +from pathlib import Path from typing import Any, AsyncGenerator, Awaitable, Callable, Dict, Generator import pytest +from playwright._impl._driver import compute_driver_executable from playwright.async_api import ( Browser, BrowserContext, BrowserType, + FrameLocator, + Locator, Page, Playwright, Selectors, async_playwright, ) +from tests.server import HTTPServer from .utils import Utils from .utils import utils as utils_object @@ -131,3 +137,75 @@ async def page(context: BrowserContext) -> AsyncGenerator[Page, None]: @pytest.fixture(scope="session") def selectors(playwright: Playwright) -> Selectors: return playwright.selectors + + +class TraceViewerPage: + def __init__(self, page: Page): + self.page = page + + @property + def actions_tree(self) -> Locator: + return self.page.get_by_test_id("actions-tree") + + @property + def action_titles(self) -> Locator: + return self.page.locator(".action-title") + + @property + def stack_frames(self) -> Locator: + return self.page.get_by_role("list", name="Stack Trace").get_by_role("listitem") + + async def select_action(self, title: str, ordinal: int = 0) -> None: + await self.page.locator(".action-title", has_text=title).nth(ordinal).click() + + async def select_snapshot(self, name: str) -> None: + await self.page.click( + f'.snapshot-tab .tabbed-pane-tab-label:has-text("{name}")' + ) + + async def snapshot_frame( + self, action_name: str, ordinal: int = 0, has_subframe: bool = False + ) -> FrameLocator: + await self.select_action(action_name, ordinal) + expected_frames = 4 if has_subframe else 3 + while len(self.page.frames) < expected_frames: + await self.page.wait_for_event("frameattached") + return self.page.frame_locator("iframe.snapshot-visible[name=snapshot]") + + async def show_source_tab(self) -> None: + await self.page.click("text='Source'") + + async def expand_action(self, title: str, ordinal: int = 0) -> None: + await self.actions_tree.locator(".tree-view-entry", has_text=title).nth( + ordinal + ).locator(".codicon-chevron-right").click() + + +@pytest.fixture +async def show_trace_viewer(browser: Browser) -> AsyncGenerator[Callable, None]: + """Fixture that provides a function to show trace viewer for a trace file.""" + + @asynccontextmanager + async def _show_trace_viewer( + trace_path: Path, + ) -> AsyncGenerator[TraceViewerPage, None]: + trace_viewer_path = ( + Path(compute_driver_executable()[0]) / "../package/lib/vite/traceViewer" + ).resolve() + + server = HTTPServer() + server.start(trace_viewer_path) + server.set_route("/trace.zip", lambda request: request.serve_file(trace_path)) + + page = await browser.new_page() + + try: + await page.goto( + f"{server.PREFIX}/index.html?trace={server.PREFIX}/trace.zip" + ) + yield TraceViewerPage(page) + finally: + await page.close() + server.stop() + + yield _show_trace_viewer diff --git a/tests/async/test_accessibility.py b/tests/async/test_accessibility.py deleted file mode 100644 index ec7b42190..000000000 --- a/tests/async/test_accessibility.py +++ /dev/null @@ -1,394 +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. - -import os -import sys - -import pytest - -from playwright.async_api import Page - - -async def test_accessibility_should_work( - page: Page, is_firefox: bool, is_chromium: bool -) -> None: - await page.set_content( - """ - Accessibility Test - - -

Inputs

- - - - - - - - - """ - ) - # autofocus happens after a delay in chrome these days - await page.wait_for_function("document.activeElement.hasAttribute('autofocus')") - - if is_firefox: - golden = { - "role": "document", - "name": "Accessibility Test", - "children": [ - {"role": "heading", "name": "Inputs", "level": 1}, - {"role": "textbox", "name": "Empty input", "focused": True}, - {"role": "textbox", "name": "readonly input", "readonly": True}, - {"role": "textbox", "name": "disabled input", "disabled": True}, - {"role": "textbox", "name": "Input with whitespace", "value": " "}, - {"role": "textbox", "name": "", "value": "value only"}, - { - "role": "textbox", - "name": "", - "value": "and a value", - }, # firefox doesn't use aria-placeholder for the name - { - "role": "textbox", - "name": "", - "value": "and a value", - "description": "This is a description!", - }, # and here - ], - } - elif is_chromium: - golden = { - "role": "WebArea", - "name": "Accessibility Test", - "children": [ - {"role": "heading", "name": "Inputs", "level": 1}, - {"role": "textbox", "name": "Empty input", "focused": True}, - {"role": "textbox", "name": "readonly input", "readonly": True}, - {"role": "textbox", "name": "disabled input", "disabled": True}, - {"role": "textbox", "name": "Input with whitespace", "value": " "}, - {"role": "textbox", "name": "", "value": "value only"}, - {"role": "textbox", "name": "placeholder", "value": "and a value"}, - { - "role": "textbox", - "name": "placeholder", - "value": "and a value", - "description": "This is a description!", - }, - ], - } - else: - golden = { - "role": "WebArea", - "name": "Accessibility Test", - "children": [ - {"role": "heading", "name": "Inputs", "level": 1}, - {"role": "textbox", "name": "Empty input", "focused": True}, - {"role": "textbox", "name": "readonly input", "readonly": True}, - {"role": "textbox", "name": "disabled input", "disabled": True}, - {"role": "textbox", "name": "Input with whitespace", "value": " "}, - {"role": "textbox", "name": "", "value": "value only"}, - {"role": "textbox", "name": "placeholder", "value": "and a value"}, - { - "role": "textbox", - "name": ( - "placeholder" - if ( - sys.platform == "darwin" - and int(os.uname().release.split(".")[0]) >= 21 - ) - else "This is a description!" - ), - "value": "and a value", - }, # webkit uses the description over placeholder for the name - ], - } - assert await page.accessibility.snapshot() == golden - - -async def test_accessibility_should_work_with_regular_text( - page: Page, is_firefox: bool -) -> None: - await page.set_content("
Hello World
") - snapshot = await page.accessibility.snapshot() - assert snapshot - assert snapshot["children"][0] == { - "role": "text leaf" if is_firefox else "text", - "name": "Hello World", - } - - -async def test_accessibility_roledescription(page: Page) -> None: - await page.set_content('

Hi

') - snapshot = await page.accessibility.snapshot() - assert snapshot - assert snapshot["children"][0]["roledescription"] == "foo" - - -async def test_accessibility_orientation(page: Page) -> None: - await page.set_content( - '11' - ) - snapshot = await page.accessibility.snapshot() - assert snapshot - assert snapshot["children"][0]["orientation"] == "vertical" - - -async def test_accessibility_autocomplete(page: Page) -> None: - await page.set_content('
hi
') - snapshot = await page.accessibility.snapshot() - assert snapshot - assert snapshot["children"][0]["autocomplete"] == "list" - - -async def test_accessibility_multiselectable(page: Page) -> None: - await page.set_content( - '
hey
' - ) - snapshot = await page.accessibility.snapshot() - assert snapshot - assert snapshot["children"][0]["multiselectable"] - - -async def test_accessibility_keyshortcuts(page: Page) -> None: - await page.set_content( - '
hey
' - ) - snapshot = await page.accessibility.snapshot() - assert snapshot - assert snapshot["children"][0]["keyshortcuts"] == "foo" - - -async def test_accessibility_filtering_children_of_leaf_nodes_should_not_report_text_nodes_inside_controls( - page: Page, is_firefox: bool -) -> None: - await page.set_content( - """ -
-
Tab1
-
Tab2
-
""" - ) - golden = { - "role": "document" if is_firefox else "WebArea", - "name": "", - "children": [ - {"role": "tab", "name": "Tab1", "selected": True}, - {"role": "tab", "name": "Tab2"}, - ], - } - assert await page.accessibility.snapshot() == golden - - -# Firefox does not support contenteditable="plaintext-only". -# WebKit rich text accessibility is iffy -@pytest.mark.only_browser("chromium") -async def test_accessibility_plain_text_field_with_role_should_not_have_children( - page: Page, -) -> None: - await page.set_content( - """ -
Edit this image:my fake image
""" - ) - snapshot = await page.accessibility.snapshot() - assert snapshot - assert snapshot["children"][0] == { - "multiline": True, - "name": "", - "role": "textbox", - "value": "Edit this image:", - } - - -@pytest.mark.only_browser("chromium") -async def test_accessibility_plain_text_field_without_role_should_not_have_content( - page: Page, -) -> None: - await page.set_content( - """ -
Edit this image:my fake image
""" - ) - snapshot = await page.accessibility.snapshot() - assert snapshot - assert snapshot["children"][0] == { - "name": "", - "role": "generic", - "value": "Edit this image:", - } - - -@pytest.mark.only_browser("chromium") -async def test_accessibility_plain_text_field_with_tabindex_and_without_role_should_not_have_content( - page: Page, -) -> None: - await page.set_content( - """ -
Edit this image:my fake image
""" - ) - snapshot = await page.accessibility.snapshot() - assert snapshot - assert snapshot["children"][0] == { - "name": "", - "role": "generic", - "value": "Edit this image:", - } - - -async def test_accessibility_non_editable_textbox_with_role_and_tabIndex_and_label_should_not_have_children( - page: Page, is_chromium: bool, is_firefox: bool -) -> None: - await page.set_content( - """ -
- this is the inner content - yo -
""" - ) - if is_firefox: - golden = { - "role": "textbox", - "name": "my favorite textbox", - "value": "this is the inner content yo", - } - elif is_chromium: - golden = { - "role": "textbox", - "name": "my favorite textbox", - "value": "this is the inner content ", - } - else: - golden = { - "role": "textbox", - "name": "my favorite textbox", - "value": "this is the inner content ", - } - snapshot = await page.accessibility.snapshot() - assert snapshot - assert snapshot["children"][0] == golden - - -async def test_accessibility_checkbox_with_and_tabIndex_and_label_should_not_have_children( - page: Page, -) -> None: - await page.set_content( - """ -
- this is the inner content - yo -
""" - ) - golden = {"role": "checkbox", "name": "my favorite checkbox", "checked": True} - snapshot = await page.accessibility.snapshot() - assert snapshot - assert snapshot["children"][0] == golden - - -async def test_accessibility_checkbox_without_label_should_not_have_children( - page: Page, is_firefox: bool -) -> None: - await page.set_content( - """ -
- this is the inner content - yo -
""" - ) - golden = { - "role": "checkbox", - "name": "this is the inner content yo", - "checked": True, - } - snapshot = await page.accessibility.snapshot() - assert snapshot - assert snapshot["children"][0] == golden - - -async def test_accessibility_should_work_a_button(page: Page) -> None: - await page.set_content("") - - button = await page.query_selector("button") - assert await page.accessibility.snapshot(root=button) == { - "role": "button", - "name": "My Button", - } - - -async def test_accessibility_should_work_an_input(page: Page) -> None: - await page.set_content('') - - input = await page.query_selector("input") - assert await page.accessibility.snapshot(root=input) == { - "role": "textbox", - "name": "My Input", - "value": "My Value", - } - - -async def test_accessibility_should_work_on_a_menu(page: Page) -> None: - await page.set_content( - """ -
-
First Item
-
Second Item
-
Third Item
-
- """ - ) - - menu = await page.query_selector('div[role="menu"]') - golden = { - "role": "menu", - "name": "My Menu", - "children": [ - {"role": "menuitem", "name": "First Item"}, - {"role": "menuitem", "name": "Second Item"}, - {"role": "menuitem", "name": "Third Item"}, - ], - } - actual = await page.accessibility.snapshot(root=menu) - assert actual - # Different per browser channel - if "orientation" in actual: - del actual["orientation"] - assert actual == golden - - -async def test_accessibility_should_return_null_when_the_element_is_no_longer_in_DOM( - page: Page, -) -> None: - await page.set_content("") - button = await page.query_selector("button") - await page.eval_on_selector("button", "button => button.remove()") - assert await page.accessibility.snapshot(root=button) is None - - -async def test_accessibility_should_show_uninteresting_nodes(page: Page) -> None: - await page.set_content( - """ -
-
- hello -
- world -
-
-
- """ - ) - - root = await page.query_selector("#root") - snapshot = await page.accessibility.snapshot(root=root, interesting_only=False) - assert snapshot - assert snapshot["role"] == "textbox" - assert "hello" in snapshot["value"] - assert "world" in snapshot["value"] - assert snapshot["children"] diff --git a/tests/async/test_assertions.py b/tests/async/test_assertions.py index 06292aa9b..49e3c3e7f 100644 --- a/tests/async/test_assertions.py +++ b/tests/async/test_assertions.py @@ -145,6 +145,32 @@ async def test_assertions_locator_to_have_class(page: Page, server: Server) -> N await expect(page.locator("div.foobar")).to_have_class("oh-no", timeout=100) +async def test_assertions_locator_to_contain_class(page: Page, server: Server) -> None: + await page.goto(server.EMPTY_PAGE) + await page.set_content("
") + locator = page.locator("div") + await expect(locator).to_contain_class("") + await expect(locator).to_contain_class("bar") + await expect(locator).to_contain_class("baz bar") + await expect(locator).to_contain_class(" bar foo ") + await expect(locator).not_to_contain_class( + " baz not-matching " + ) # Strip whitespace and match individual classes + with pytest.raises(AssertionError) as excinfo: + await expect(locator).to_contain_class("does-not-exist", timeout=100) + + assert excinfo.match("Locator expected to contain class 'does-not-exist'") + assert excinfo.match("Actual value: foo bar baz") + assert excinfo.match('Expect "to_contain_class" with timeout 100ms') + + await page.set_content( + '
' + ) + await expect(locator).to_contain_class(["foo", "hello", "baz"]) + await expect(locator).not_to_contain_class(["not-there", "hello", "baz"]) + await expect(locator).not_to_contain_class(["foo", "hello"]) + + async def test_assertions_locator_to_have_count(page: Page, server: Server) -> None: await page.goto(server.EMPTY_PAGE) await page.set_content("
kek
kek
") @@ -490,7 +516,7 @@ async def test_to_have_values_fails_when_multiple_not_specified( ) locator = page.locator("select") await locator.select_option(["B"]) - with pytest.raises(Error) as excinfo: + with pytest.raises(AssertionError) as excinfo: await expect(locator).to_have_values(["R", "G"], timeout=500) assert "Error: Not a select element with a multiple attribute" in str(excinfo.value) @@ -504,7 +530,7 @@ async def test_to_have_values_fails_when_not_a_select_element( """ ) locator = page.locator("input") - with pytest.raises(Error) as excinfo: + with pytest.raises(AssertionError) as excinfo: await expect(locator).to_have_values(["R", "G"], timeout=500) assert "Error: Not a select element with a multiple attribute" in str(excinfo.value) @@ -526,6 +552,35 @@ async def test_assertions_locator_to_be_checked(page: Page, server: Server) -> N await expect(my_checkbox).to_be_checked() +async def test_assertions_boolean_checked_with_intermediate_true(page: Page) -> None: + await page.set_content("") + await page.locator("input").evaluate("e => e.indeterminate = true") + await expect(page.locator("input")).to_be_checked(indeterminate=True) + + +async def test_assertions_boolean_checked_with_intermediate_true_and_checked( + page: Page, +) -> None: + await page.set_content("") + await page.locator("input").evaluate("e => e.indeterminate = true") + with pytest.raises( + AssertionError, match="Can't assert indeterminate and checked at the same time" + ): + await expect(page.locator("input")).to_be_checked( + checked=False, indeterminate=True + ) + + +async def test_assertions_boolean_fail_with_indeterminate_true(page: Page) -> None: + await page.set_content("") + with pytest.raises( + AssertionError, match='Expect "to_be_checked" with timeout 1000ms' + ): + await expect(page.locator("input")).to_be_checked( + indeterminate=True, timeout=1000 + ) + + async def test_assertions_locator_to_be_disabled_enabled( page: Page, server: Server ) -> None: @@ -603,7 +658,7 @@ async def test_assertions_locator_to_be_editable_throws( await page.goto(server.EMPTY_PAGE) await page.set_content("") with pytest.raises( - Error, + AssertionError, match=r"Element is not an ,