├── .azure-pipelines ├── guardian │ └── SDL │ │ └── .gdnsuppress └── publish.yml ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ ├── documentation.yml │ ├── feature.yml │ ├── question.yml │ └── regression.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── publish.yml │ ├── publish_docker.yml │ └── test_docker.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ROLLING.md ├── SECURITY.md ├── SUPPORT.md ├── conda_build_config_linux_aarch64.yaml ├── conda_build_config_osx_arm64.yaml ├── examples └── todomvc │ ├── mvctests │ ├── __init__.py │ ├── test_clear_completed_button.py │ ├── test_counter.py │ ├── test_editing.py │ ├── test_item.py │ ├── test_mark_all_as_completed.py │ ├── test_new_todo.py │ ├── test_persistence.py │ ├── test_routing.py │ └── utils.py │ └── requirements.txt ├── local-requirements.txt ├── meta.yaml ├── playwright ├── __init__.py ├── __main__.py ├── _impl │ ├── __init__.py │ ├── __pyinstaller │ │ ├── __init__.py │ │ ├── hook-playwright.async_api.py │ │ └── hook-playwright.sync_api.py │ ├── _accessibility.py │ ├── _api_structures.py │ ├── _artifact.py │ ├── _assertions.py │ ├── _async_base.py │ ├── _browser.py │ ├── _browser_context.py │ ├── _browser_type.py │ ├── _cdp_session.py │ ├── _clock.py │ ├── _connection.py │ ├── _console_message.py │ ├── _dialog.py │ ├── _download.py │ ├── _driver.py │ ├── _element_handle.py │ ├── _errors.py │ ├── _event_context_manager.py │ ├── _fetch.py │ ├── _file_chooser.py │ ├── _frame.py │ ├── _glob.py │ ├── _greenlets.py │ ├── _har_router.py │ ├── _helper.py │ ├── _impl_to_api_mapping.py │ ├── _input.py │ ├── _js_handle.py │ ├── _json_pipe.py │ ├── _local_utils.py │ ├── _locator.py │ ├── _map.py │ ├── _network.py │ ├── _object_factory.py │ ├── _page.py │ ├── _path_utils.py │ ├── _playwright.py │ ├── _selectors.py │ ├── _set_input_files_helpers.py │ ├── _str_utils.py │ ├── _stream.py │ ├── _sync_base.py │ ├── _tracing.py │ ├── _transport.py │ ├── _video.py │ ├── _waiter.py │ ├── _web_error.py │ └── _writable_stream.py ├── async_api │ ├── __init__.py │ ├── _context_manager.py │ └── _generated.py ├── py.typed └── sync_api │ ├── __init__.py │ ├── _context_manager.py │ └── _generated.py ├── pyproject.toml ├── requirements.txt ├── scripts ├── documentation_provider.py ├── example_async.py ├── example_sync.py ├── expected_api_mismatch.txt ├── generate_api.py ├── generate_async_api.py ├── generate_sync_api.py ├── update_api.sh └── update_versions.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── assets │ ├── beforeunload.html │ ├── client-certificates │ │ ├── README.md │ │ ├── client │ │ │ ├── self-signed │ │ │ │ ├── cert.pem │ │ │ │ ├── csr.pem │ │ │ │ └── key.pem │ │ │ └── trusted │ │ │ │ ├── cert.pem │ │ │ │ ├── csr.pem │ │ │ │ └── key.pem │ │ └── server │ │ │ ├── server_cert.pem │ │ │ └── server_key.pem │ ├── client.py │ ├── consolelog.html │ ├── csp.html │ ├── digits │ │ ├── 0.png │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ └── 9.png │ ├── dom.html │ ├── download-blob.html │ ├── drag-n-drop.html │ ├── dummy_bad_browser_executable.js │ ├── empty.html │ ├── error.html │ ├── es6 │ │ ├── .eslintrc │ │ ├── es6import.js │ │ ├── es6module.js │ │ └── es6pathimport.js │ ├── file-to-upload-2.txt │ ├── file-to-upload.txt │ ├── frames │ │ ├── child-redirect.html │ │ ├── frame.html │ │ ├── frameset.html │ │ ├── nested-frames.html │ │ ├── one-frame.html │ │ ├── redirect-my-parent.html │ │ ├── script.js │ │ ├── style.css │ │ └── two-frames.html │ ├── geolocation.html │ ├── global-var.html │ ├── grid.html │ ├── har-fulfill.har │ ├── har-redirect.har │ ├── har-sha1-main-response.txt │ ├── har-sha1.har │ ├── har.html │ ├── headings.html │ ├── historyapi.html │ ├── injectedfile.js │ ├── injectedstyle.css │ ├── input │ │ ├── animating-button.html │ │ ├── button.html │ │ ├── checkbox.html │ │ ├── fileupload-multi.html │ │ ├── fileupload.html │ │ ├── folderupload.html │ │ ├── handle-locator.html │ │ ├── keyboard.html │ │ ├── mouse-helper.js │ │ ├── rotatedButton.html │ │ ├── scrollable.html │ │ ├── select.html │ │ ├── textarea.html │ │ └── touches.html │ ├── mobile.html │ ├── networkidle.html │ ├── networkidle.js │ ├── offscreenbuttons.html │ ├── one-style.css │ ├── one-style.html │ ├── playground.html │ ├── popup │ │ ├── popup.html │ │ └── window-open.html │ ├── pptr.png │ ├── react.html │ ├── react │ │ ├── react-dom@16.13.1.production.min.js │ │ └── react@16.13.1.production.min.js │ ├── sectionselectorengine.js │ ├── self-request.html │ ├── serviceworkers │ │ ├── empty │ │ │ ├── sw.html │ │ │ └── sw.js │ │ ├── fetch │ │ │ ├── style.css │ │ │ ├── sw.html │ │ │ └── sw.js │ │ └── fetchdummy │ │ │ ├── sw.html │ │ │ └── sw.js │ ├── shadow.html │ ├── simple-extension │ │ ├── content-script.js │ │ ├── index.js │ │ └── manifest.json │ ├── simple.json │ ├── title.html │ ├── worker │ │ ├── worker.html │ │ └── worker.js │ └── wrappedlink.html ├── async │ ├── __init__.py │ ├── conftest.py │ ├── test_accessibility.py │ ├── test_add_init_script.py │ ├── test_assertions.py │ ├── test_asyncio.py │ ├── test_browser.py │ ├── test_browsercontext.py │ ├── test_browsercontext_add_cookies.py │ ├── test_browsercontext_clearcookies.py │ ├── test_browsercontext_client_certificates.py │ ├── test_browsercontext_cookies.py │ ├── test_browsercontext_events.py │ ├── test_browsercontext_proxy.py │ ├── test_browsercontext_request_fallback.py │ ├── test_browsercontext_request_intercept.py │ ├── test_browsercontext_route.py │ ├── test_browsercontext_service_worker_policy.py │ ├── test_browsercontext_storage_state.py │ ├── test_browsertype_connect.py │ ├── test_browsertype_connect_cdp.py │ ├── test_cdp_session.py │ ├── test_check.py │ ├── test_chromium_tracing.py │ ├── test_click.py │ ├── test_console.py │ ├── test_context_manager.py │ ├── test_defaultbrowsercontext.py │ ├── test_device_descriptors.py │ ├── test_dialog.py │ ├── test_dispatch_event.py │ ├── test_download.py │ ├── test_element_handle.py │ ├── test_element_handle_wait_for_element_state.py │ ├── test_emulation_focus.py │ ├── test_expect_misc.py │ ├── test_fetch_browser_context.py │ ├── test_fetch_global.py │ ├── test_fill.py │ ├── test_focus.py │ ├── test_frames.py │ ├── test_geolocation.py │ ├── test_har.py │ ├── test_headful.py │ ├── test_ignore_https_errors.py │ ├── test_input.py │ ├── test_issues.py │ ├── test_jshandle.py │ ├── test_keyboard.py │ ├── test_launcher.py │ ├── test_listeners.py │ ├── test_locators.py │ ├── test_navigation.py │ ├── test_network.py │ ├── test_page.py │ ├── test_page_add_locator_handler.py │ ├── test_page_aria_snapshot.py │ ├── test_page_base_url.py │ ├── test_page_clock.py │ ├── test_page_evaluate.py │ ├── test_page_network_request.py │ ├── test_page_network_response.py │ ├── test_page_request_fallback.py │ ├── test_page_request_gc.py │ ├── test_page_request_intercept.py │ ├── test_page_route.py │ ├── test_page_select_option.py │ ├── test_pdf.py │ ├── test_popup.py │ ├── test_proxy.py │ ├── test_queryselector.py │ ├── test_request_continue.py │ ├── test_request_fulfill.py │ ├── test_request_intercept.py │ ├── test_resource_timing.py │ ├── test_route_web_socket.py │ ├── test_screenshot.py │ ├── test_selector_generator.py │ ├── test_selectors_get_by.py │ ├── test_selectors_misc.py │ ├── test_selectors_text.py │ ├── test_tap.py │ ├── test_tracing.py │ ├── test_unroute_behavior.py │ ├── test_video.py │ ├── test_wait_for_function.py │ ├── test_wait_for_url.py │ ├── test_websocket.py │ ├── test_worker.py │ └── utils.py ├── common │ ├── __init__.py │ ├── test_collect_handles.py │ ├── test_events.py │ ├── test_signals.py │ └── test_threads.py ├── conftest.py ├── golden-chromium │ ├── grid-cell-0.png │ ├── mask-should-work-with-element-handle.png │ ├── mask-should-work-with-locator.png │ ├── mask-should-work-with-page.png │ ├── mock-binary-response.png │ ├── mock-svg.png │ ├── screenshot-element-bounding-box.png │ └── screenshot-sanity.png ├── golden-firefox │ ├── grid-cell-0.png │ ├── mask-should-work-with-element-handle.png │ ├── mask-should-work-with-locator.png │ ├── mask-should-work-with-page.png │ ├── mock-binary-response.png │ ├── mock-svg.png │ ├── screenshot-element-bounding-box.png │ └── screenshot-sanity.png ├── golden-webkit │ ├── grid-cell-0.png │ ├── mask-should-work-with-element-handle.png │ ├── mask-should-work-with-locator.png │ ├── mask-should-work-with-page.png │ ├── mock-binary-response.png │ ├── mock-svg.png │ ├── screenshot-element-bounding-box.png │ └── screenshot-sanity.png ├── server.py ├── sync │ ├── __init__.py │ ├── conftest.py │ ├── test_accessibility.py │ ├── test_add_init_script.py │ ├── test_assertions.py │ ├── test_browser.py │ ├── test_browsercontext_client_certificates.py │ ├── test_browsercontext_events.py │ ├── test_browsercontext_request_fallback.py │ ├── test_browsercontext_request_intercept.py │ ├── test_browsercontext_service_worker_policy.py │ ├── test_browsercontext_storage_state.py │ ├── test_browsertype_connect.py │ ├── test_browsertype_connect_cdp.py │ ├── test_cdp_session.py │ ├── test_check.py │ ├── test_console.py │ ├── test_context_manager.py │ ├── test_element_handle.py │ ├── test_element_handle_wait_for_element_state.py │ ├── test_expect_misc.py │ ├── test_fetch_browser_context.py │ ├── test_fetch_global.py │ ├── test_fill.py │ ├── test_har.py │ ├── test_input.py │ ├── test_launcher.py │ ├── test_listeners.py │ ├── test_locator_get_by.py │ ├── test_locators.py │ ├── test_network.py │ ├── test_page.py │ ├── test_page_add_locator_handler.py │ ├── test_page_aria_snapshot.py │ ├── test_page_clock.py │ ├── test_page_network_response.py │ ├── test_page_request_fallback.py │ ├── test_page_request_gc.py │ ├── test_page_request_intercept.py │ ├── test_page_select_option.py │ ├── test_pdf.py │ ├── test_queryselector.py │ ├── test_request_fulfill.py │ ├── test_request_intercept.py │ ├── test_resource_timing.py │ ├── test_route_web_socket.py │ ├── test_selectors_misc.py │ ├── test_sync.py │ ├── test_tap.py │ ├── test_tracing.py │ ├── test_unroute_behavior.py │ ├── test_video.py │ └── utils.py ├── test_installation.py ├── test_reference_count_async.py ├── testserver │ ├── cert.pem │ └── key.pem └── utils.py └── utils ├── docker ├── .gitignore ├── Dockerfile.jammy ├── Dockerfile.noble ├── build.sh └── publish_docker.sh └── linting └── check_file_header.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # text files must be lf for golden file tests to work 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Join our Discord Server 4 | url: https://aka.ms/playwright/discord 5 | about: Ask questions and discuss with other community members 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 📖 2 | description: Submit a request to add or update documentation 3 | title: '[Docs]: ' 4 | labels: ['Documentation :book:'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | ### Thank you for helping us improve our documentation! 10 | Please be sure you are looking at [the Next version of the documentation](https://playwright.dev/python/docs/next/intro) before opening an issue here. 11 | - type: textarea 12 | id: links 13 | attributes: 14 | label: Page(s) 15 | description: | 16 | Links to one or more documentation pages that should be modified. 17 | If you are reporting an issue with a specific section of a page, try to link directly to the nearest anchor. 18 | If you are suggesting that a new page be created, link to the parent of the proposed page. 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: description 23 | attributes: 24 | label: Description 25 | description: | 26 | Describe the change you are requesting. 27 | If the issue pertains to a single function or matcher, be sure to specify the entire call signature. 28 | validations: 29 | required: true 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 🚀 2 | description: Submit a proposal for a new feature 3 | title: '[Feature]: ' 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ### Thank you for taking the time to suggest a new feature! 9 | - type: textarea 10 | id: description 11 | attributes: 12 | label: '🚀 Feature Request' 13 | description: A clear and concise description of what the feature is. 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: example 18 | attributes: 19 | label: Example 20 | description: Describe how this feature would be used. 21 | validations: 22 | required: false 23 | - type: textarea 24 | id: motivation 25 | attributes: 26 | label: Motivation 27 | description: | 28 | Outline your motivation for the proposal. How will it make Playwright better? 29 | validations: 30 | required: true 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yml: -------------------------------------------------------------------------------- 1 | name: 'Questions / Help 💬' 2 | description: If you have questions, please check StackOverflow or Discord 3 | title: '[Please read the message below]' 4 | labels: [':speech_balloon: Question'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | ## Questions and Help 💬 10 | 11 | This issue tracker is reserved for bug reports and feature requests. 12 | 13 | For anything else, such as questions or getting help, please see: 14 | 15 | - [The Playwright documentation](https://playwright.dev) 16 | - [Our Discord server](https://aka.ms/playwright/discord) 17 | - type: checkboxes 18 | id: no-post 19 | attributes: 20 | label: | 21 | Please do not submit this issue. 22 | description: | 23 | > [!IMPORTANT] 24 | > This issue will be closed. 25 | options: 26 | - label: I understand 27 | required: true 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | groups: 12 | actions: 13 | patterns: 14 | - "*" 15 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | on: 3 | release: 4 | types: [published] 5 | workflow_dispatch: 6 | jobs: 7 | deploy-conda: 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | include: 12 | - os: ubuntu-latest 13 | target-platform: linux-x86_64 14 | - os: ubuntu-latest 15 | target-platform: linux-aarch64 16 | - os: windows-latest 17 | target-platform: win-64 18 | - os: macos-latest-large 19 | target-platform: osx-intel 20 | - os: macos-latest-xlarge 21 | target-platform: osx-arm64 22 | runs-on: ${{ matrix.os }} 23 | defaults: 24 | run: 25 | # Required for conda-incubator/setup-miniconda@v3 26 | shell: bash -el {0} 27 | steps: 28 | - uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 0 31 | - name: Get conda 32 | uses: conda-incubator/setup-miniconda@v3 33 | with: 34 | python-version: 3.9 35 | channels: conda-forge 36 | miniconda-version: latest 37 | - name: Prepare 38 | run: conda install anaconda-client conda-build conda-verify 39 | - name: Build and Upload 40 | env: 41 | ANACONDA_API_TOKEN: ${{ secrets.ANACONDA_API_TOKEN }} 42 | run: | 43 | conda config --set anaconda_upload yes 44 | if [ "${{ matrix.target-platform }}" == "osx-arm64" ]; then 45 | conda build --user microsoft . -m conda_build_config_osx_arm64.yaml 46 | elif [ "${{ matrix.target-platform }}" == "linux-aarch64" ]; then 47 | conda build --user microsoft . -m conda_build_config_linux_aarch64.yaml 48 | else 49 | conda build --user microsoft . 50 | fi 51 | -------------------------------------------------------------------------------- /.github/workflows/publish_docker.yml: -------------------------------------------------------------------------------- 1 | name: "publish release - Docker" 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | publish-docker-release: 10 | name: "publish to DockerHub" 11 | runs-on: ubuntu-22.04 12 | if: github.repository == 'microsoft/playwright-python' 13 | permissions: 14 | id-token: write # This is required for OIDC login (azure/login) to succeed 15 | contents: read # This is required for actions/checkout to succeed 16 | environment: Docker 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Azure login 20 | uses: azure/login@v2 21 | with: 22 | client-id: ${{ secrets.AZURE_DOCKER_CLIENT_ID }} 23 | tenant-id: ${{ secrets.AZURE_DOCKER_TENANT_ID }} 24 | subscription-id: ${{ secrets.AZURE_DOCKER_SUBSCRIPTION_ID }} 25 | - name: Login to ACR via OIDC 26 | run: az acr login --name playwright 27 | - name: Set up Python 28 | uses: actions/setup-python@v5 29 | with: 30 | python-version: "3.10" 31 | - name: Set up Docker QEMU for arm64 docker builds 32 | uses: docker/setup-qemu-action@v3 33 | with: 34 | platforms: arm64 35 | - name: Install dependencies & browsers 36 | run: | 37 | python -m pip install --upgrade pip 38 | pip install -r local-requirements.txt 39 | pip install -r requirements.txt 40 | pip install -e . 41 | - run: ./utils/docker/publish_docker.sh stable 42 | -------------------------------------------------------------------------------- /.github/workflows/test_docker.yml: -------------------------------------------------------------------------------- 1 | name: Test Docker 2 | on: 3 | push: 4 | paths: 5 | - '.github/workflows/test_docker.yml' 6 | - 'setup.py' 7 | - '**/Dockerfile.*' 8 | branches: 9 | - main 10 | - release-* 11 | pull_request: 12 | paths: 13 | - '.github/workflows/test_docker.yml' 14 | - 'setup.py' 15 | - '**/Dockerfile.*' 16 | branches: 17 | - main 18 | - release-* 19 | jobs: 20 | build: 21 | timeout-minutes: 120 22 | runs-on: ${{ matrix.runs-on }} 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | docker-image-variant: 27 | - jammy 28 | - noble 29 | runs-on: 30 | - ubuntu-24.04 31 | - ubuntu-24.04-arm 32 | steps: 33 | - uses: actions/checkout@v4 34 | - name: Set up Python 35 | uses: actions/setup-python@v5 36 | with: 37 | python-version: "3.10" 38 | - name: Install dependencies 39 | run: | 40 | python -m pip install --upgrade pip 41 | pip install -r local-requirements.txt 42 | pip install -r requirements.txt 43 | pip install -e . 44 | - name: Build Docker image 45 | run: | 46 | ARCH="${{ matrix.runs-on == 'ubuntu-24.04-arm' && 'arm64' || 'amd64' }}" 47 | bash utils/docker/build.sh --$ARCH ${{ matrix.docker-image-variant }} playwright-python:localbuild-${{ matrix.docker-image-variant }} 48 | - name: Test 49 | run: | 50 | 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)" 51 | # Fix permissions for Git inside the container 52 | docker exec "${CONTAINER_ID}" chown -R root:root /root/playwright 53 | docker exec "${CONTAINER_ID}" pip install -r local-requirements.txt 54 | docker exec "${CONTAINER_ID}" pip install -r requirements.txt 55 | docker exec "${CONTAINER_ID}" pip install -e . 56 | docker exec "${CONTAINER_ID}" python -m build --wheel 57 | docker exec "${CONTAINER_ID}" xvfb-run pytest tests/sync/ 58 | docker exec "${CONTAINER_ID}" xvfb-run pytest tests/async/ 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__/ 2 | driver/ 3 | playwright/driver/ 4 | playwright.egg-info/ 5 | build/ 6 | dist/ 7 | venv/ 8 | .idea/ 9 | **/*.pyc 10 | env/ 11 | htmlcov/ 12 | .coverage* 13 | .DS_Store 14 | .vscode/ 15 | .eggs 16 | _repo_version.py 17 | coverage.xml 18 | junit/ 19 | htmldocs/ 20 | utils/docker/dist/ 21 | Pipfile 22 | Pipfile.lock 23 | .venv/ 24 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v5.0.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | exclude: tests/assets/har-sha1-main-response.txt 10 | - id: check-yaml 11 | - id: check-toml 12 | - id: requirements-txt-fixer 13 | - id: check-ast 14 | - id: check-builtin-literals 15 | - id: check-executables-have-shebangs 16 | - id: check-merge-conflict 17 | - repo: https://github.com/psf/black 18 | rev: 24.8.0 19 | hooks: 20 | - id: black 21 | - repo: https://github.com/pre-commit/mirrors-mypy 22 | rev: v1.11.2 23 | hooks: 24 | - id: mypy 25 | additional_dependencies: [types-pyOpenSSL==24.1.0.20240722, types-requests==2.32.0.20240914] 26 | - repo: https://github.com/pycqa/flake8 27 | rev: 7.1.1 28 | hooks: 29 | - id: flake8 30 | - repo: https://github.com/pycqa/isort 31 | rev: 5.13.2 32 | hooks: 33 | - id: isort 34 | - repo: local 35 | hooks: 36 | - id: pyright 37 | name: pyright 38 | entry: pyright 39 | language: node 40 | pass_filenames: false 41 | types: [python] 42 | additional_dependencies: ["pyright@1.1.384"] 43 | - repo: local 44 | hooks: 45 | - id: check-license-header 46 | name: Check License Header 47 | entry: ./utils/linting/check_file_header.py 48 | language: python 49 | types: [python] 50 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## How to Contribute 4 | 5 | ### Configuring python environment 6 | 7 | The project development requires Python version 3.9+. To set it as default in the environment run the following commands: 8 | 9 | ```sh 10 | # You may need to install python 3.9 venv if it's missing, on Ubuntu just run `sudo apt-get install python3.9-venv` 11 | python3.9 -m venv env 12 | source ./env/bin/activate 13 | ``` 14 | 15 | Install required dependencies: 16 | 17 | ```sh 18 | python -m pip install --upgrade pip 19 | pip install -r local-requirements.txt 20 | ``` 21 | 22 | Build and install drivers: 23 | 24 | ```sh 25 | pip install -e . 26 | python -m build --wheel 27 | ``` 28 | 29 | Run tests: 30 | 31 | ```sh 32 | pytest --browser chromium 33 | ``` 34 | 35 | Checking for typing errors 36 | 37 | ```sh 38 | mypy playwright 39 | ``` 40 | 41 | Format the code 42 | 43 | ```sh 44 | pre-commit install 45 | pre-commit run --all-files 46 | ``` 47 | 48 | For more details look at the [CI configuration](./.github/workflows/ci.yml). 49 | 50 | Collect coverage 51 | 52 | ```sh 53 | pytest --browser chromium --cov-report html --cov=playwright 54 | open htmlcov/index.html 55 | ``` 56 | 57 | ### Regenerating APIs 58 | 59 | ```bash 60 | ./scripts/update_api.sh 61 | pre-commit run --all-files 62 | ``` 63 | 64 | ## Contributor License Agreement 65 | 66 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 67 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 68 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 69 | 70 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 71 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 72 | provided by the bot. You will only need to do this once across all repos using our CLA. 73 | 74 | ## Code of Conduct 75 | 76 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 77 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 78 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🎭 [Playwright](https://playwright.dev) for Python [![PyPI version](https://badge.fury.io/py/playwright.svg)](https://pypi.python.org/pypi/playwright/) [![Anaconda version](https://img.shields.io/conda/v/microsoft/playwright)](https://anaconda.org/Microsoft/playwright) [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord) 2 | 3 | Playwright is a Python library to automate [Chromium](https://www.chromium.org/Home), [Firefox](https://www.mozilla.org/en-US/firefox/new/) and [WebKit](https://webkit.org/) browsers with a single API. Playwright delivers automation that is **ever-green**, **capable**, **reliable** and **fast**. [See how Playwright is better](https://playwright.dev/python). 4 | 5 | | | Linux | macOS | Windows | 6 | | :--- | :---: | :---: | :---: | 7 | | Chromium 136.0.7103.25 | ✅ | ✅ | ✅ | 8 | | WebKit 18.4 | ✅ | ✅ | ✅ | 9 | | Firefox 137.0 | ✅ | ✅ | ✅ | 10 | 11 | ## Documentation 12 | 13 | [https://playwright.dev/python/docs/intro](https://playwright.dev/python/docs/intro) 14 | 15 | ## API Reference 16 | 17 | [https://playwright.dev/python/docs/api/class-playwright](https://playwright.dev/python/docs/api/class-playwright) 18 | 19 | ## Example 20 | 21 | ```py 22 | from playwright.sync_api import sync_playwright 23 | 24 | with sync_playwright() as p: 25 | for browser_type in [p.chromium, p.firefox, p.webkit]: 26 | browser = browser_type.launch() 27 | page = browser.new_page() 28 | page.goto('http://playwright.dev') 29 | page.screenshot(path=f'example-{browser_type.name}.png') 30 | browser.close() 31 | ``` 32 | 33 | ```py 34 | import asyncio 35 | from playwright.async_api import async_playwright 36 | 37 | async def main(): 38 | async with async_playwright() as p: 39 | for browser_type in [p.chromium, p.firefox, p.webkit]: 40 | browser = await browser_type.launch() 41 | page = await browser.new_page() 42 | await page.goto('http://playwright.dev') 43 | await page.screenshot(path=f'example-{browser_type.name}.png') 44 | await browser.close() 45 | 46 | asyncio.run(main()) 47 | ``` 48 | 49 | ## Other languages 50 | 51 | More comfortable in another programming language? [Playwright](https://playwright.dev) is also available in 52 | - [Node.js (JavaScript / TypeScript)](https://playwright.dev/docs/intro), 53 | - [.NET](https://playwright.dev/dotnet/docs/intro), 54 | - [Java](https://playwright.dev/java/docs/intro). 55 | -------------------------------------------------------------------------------- /ROLLING.md: -------------------------------------------------------------------------------- 1 | # Rolling Playwright-Python to the latest Playwright driver 2 | 3 | * checkout repo: `git clone https://github.com/microsoft/playwright-python` 4 | * make sure local python is 3.9 5 | * create virtual environment, if don't have one: `python -m venv env` 6 | * activate venv: `source env/bin/activate` 7 | * install all deps: 8 | - `python -m pip install --upgrade pip` 9 | - `pip install -r local-requirements.txt` 10 | - `pre-commit install` 11 | - `pip install -e .` 12 | * change driver version in `setup.py` 13 | * download new driver: `python -m build --wheel` 14 | * generate API: `./scripts/update_api.sh` 15 | * commit changes & send PR 16 | * wait for bots to pass & merge the PR 17 | 18 | 19 | ## Fix typing issues with Playwright ToT 20 | 21 | 1. `cd playwright` 22 | 1. `API_JSON_MODE=1 node utils/doclint/generateApiJson.js > ../playwright-python/playwright/driver/package/api.json` 23 | 1. `./scripts/update_api.sh` 24 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub issues to track bugs and feature requests. Please search the [existing issues][gh-issues] before filing new ones to avoid duplicates. For new issues, file your bug or feature request as a new issue using corresponding template. 6 | 7 | For help and questions about using this project, please see the [docs site for Playwright for Python][docs]. 8 | 9 | Join our community [Discord Server][discord-server] to connect with other developers using Playwright and ask questions in our 'help-playwright' forum. 10 | 11 | ## Microsoft Support Policy 12 | 13 | Support for Playwright for Python is limited to the resources listed above. 14 | 15 | [gh-issues]: https://github.com/microsoft/playwright-python/issues/ 16 | [docs]: https://playwright.dev/python/ 17 | [discord-server]: https://aka.ms/playwright/discord 18 | -------------------------------------------------------------------------------- /conda_build_config_linux_aarch64.yaml: -------------------------------------------------------------------------------- 1 | target_platform: 2 | - linux-aarch64 3 | -------------------------------------------------------------------------------- /conda_build_config_osx_arm64.yaml: -------------------------------------------------------------------------------- 1 | target_platform: 2 | - osx-arm64 3 | -------------------------------------------------------------------------------- /examples/todomvc/mvctests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/examples/todomvc/mvctests/__init__.py -------------------------------------------------------------------------------- /examples/todomvc/mvctests/test_clear_completed_button.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from typing import Generator 15 | 16 | import pytest 17 | 18 | from playwright.sync_api import Page, expect 19 | 20 | from .utils import TODO_ITEMS, create_default_todos 21 | 22 | 23 | @pytest.fixture(autouse=True) 24 | def run_around_tests(page: Page) -> Generator[None, None, None]: 25 | # setup before a test 26 | page.goto("https://demo.playwright.dev/todomvc") 27 | create_default_todos(page) 28 | # run the actual test 29 | yield 30 | # run any cleanup code 31 | 32 | 33 | def test_should_display_the_correct_text(page: Page) -> None: 34 | page.locator(".todo-list li .toggle").first.check() 35 | expect(page.locator(".clear-completed")).to_have_text("Clear completed") 36 | 37 | 38 | def test_should_clear_completed_items_when_clicked(page: Page) -> None: 39 | todo_items = page.locator(".todo-list li") 40 | todo_items.nth(1).locator(".toggle").check() 41 | page.locator(".clear-completed").click() 42 | expect(todo_items).to_have_count(2) 43 | expect(todo_items).to_have_text([TODO_ITEMS[0], TODO_ITEMS[2]]) 44 | 45 | 46 | def test_should_be_hidden_when_there_are_no_items_that_are_completed( 47 | page: Page, 48 | ) -> None: 49 | page.locator(".todo-list li .toggle").first.check() 50 | page.locator(".clear-completed").click() 51 | expect(page.locator(".clear-completed")).to_be_hidden() 52 | -------------------------------------------------------------------------------- /examples/todomvc/mvctests/test_counter.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from typing import Generator 15 | 16 | import pytest 17 | 18 | from playwright.sync_api import Page, expect 19 | 20 | from .utils import TODO_ITEMS, assert_number_of_todos_in_local_storage 21 | 22 | 23 | @pytest.fixture(autouse=True) 24 | def run_around_tests(page: Page) -> Generator[None, None, None]: 25 | # setup before a test 26 | page.goto("https://demo.playwright.dev/todomvc") 27 | # run the actual test 28 | yield 29 | # run any cleanup code 30 | 31 | 32 | def test_should_display_the_current_number_of_todo_items(page: Page) -> None: 33 | page.locator(".new-todo").fill(TODO_ITEMS[0]) 34 | page.locator(".new-todo").press("Enter") 35 | expect(page.locator(".todo-count")).to_contain_text("1") 36 | 37 | page.locator(".new-todo").fill(TODO_ITEMS[1]) 38 | page.locator(".new-todo").press("Enter") 39 | expect(page.locator(".todo-count")).to_contain_text("2") 40 | 41 | assert_number_of_todos_in_local_storage(page, 2) 42 | -------------------------------------------------------------------------------- /examples/todomvc/mvctests/test_persistence.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from typing import Generator 15 | 16 | import pytest 17 | 18 | from playwright.sync_api import Page, expect 19 | 20 | from .utils import TODO_ITEMS, check_number_of_completed_todos_in_local_storage 21 | 22 | 23 | @pytest.fixture(autouse=True) 24 | def run_around_tests(page: Page) -> Generator[None, None, None]: 25 | # setup before a test 26 | page.goto("https://demo.playwright.dev/todomvc") 27 | # run the actual test 28 | yield 29 | # run any cleanup code 30 | 31 | 32 | def test_should_persist_its_data(page: Page) -> None: 33 | for item in TODO_ITEMS[:2]: 34 | page.locator(".new-todo").fill(item) 35 | page.locator(".new-todo").press("Enter") 36 | 37 | todo_items = page.locator(".todo-list li") 38 | todo_items.nth(0).locator(".toggle").check() 39 | expect(todo_items).to_have_text([TODO_ITEMS[0], TODO_ITEMS[1]]) 40 | expect(todo_items).to_have_class(["completed", ""]) 41 | 42 | # Ensure there is 1 completed item. 43 | check_number_of_completed_todos_in_local_storage(page, 1) 44 | 45 | # Now reload. 46 | page.reload() 47 | expect(todo_items).to_have_text([TODO_ITEMS[0], TODO_ITEMS[1]]) 48 | expect(todo_items).to_have_class(["completed", ""]) 49 | -------------------------------------------------------------------------------- /examples/todomvc/mvctests/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from playwright.sync_api import Page 15 | 16 | TODO_ITEMS = ["buy some cheese", "feed the cat", "book a doctors appointment"] 17 | 18 | 19 | def create_default_todos(page: Page) -> None: 20 | for item in TODO_ITEMS: 21 | page.locator(".new-todo").fill(item) 22 | page.locator(".new-todo").press("Enter") 23 | 24 | 25 | def check_number_of_completed_todos_in_local_storage(page: Page, expected: int) -> None: 26 | assert ( 27 | page.evaluate( 28 | "JSON.parse(localStorage['react-todos']).filter(i => i.completed).length" 29 | ) 30 | == expected 31 | ) 32 | 33 | 34 | def assert_number_of_todos_in_local_storage(page: Page, expected: int) -> None: 35 | assert len(page.evaluate("JSON.parse(localStorage['react-todos'])")) == expected 36 | 37 | 38 | def check_todos_in_local_storage(page: Page, title: str) -> None: 39 | assert title in page.evaluate( 40 | "JSON.parse(localStorage['react-todos']).map(i => i.title)" 41 | ) 42 | -------------------------------------------------------------------------------- /examples/todomvc/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest-playwright 2 | -------------------------------------------------------------------------------- /local-requirements.txt: -------------------------------------------------------------------------------- 1 | autobahn==23.1.2 2 | black==25.1.0 3 | build==1.2.2.post1 4 | flake8==7.2.0 5 | mypy==1.16.0 6 | objgraph==3.6.2 7 | Pillow==11.2.1 8 | pixelmatch==0.3.0 9 | pre-commit==3.5.0 10 | pyOpenSSL==25.1.0 11 | pytest==8.4.0 12 | pytest-asyncio==1.0.0 13 | pytest-cov==6.1.1 14 | pytest-repeat==0.9.4 15 | pytest-rerunfailures==15.1 16 | pytest-timeout==2.4.0 17 | pytest-xdist==3.6.1 18 | requests==2.32.3 19 | service_identity==24.2.0 20 | twisted==24.11.0 21 | types-pyOpenSSL==24.1.0.20240722 22 | types-requests==2.32.0.20250602 23 | -------------------------------------------------------------------------------- /meta.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: playwright 3 | version: "{{ environ.get('GIT_DESCRIBE_TAG') | replace('v', '') }}" 4 | 5 | source: 6 | path: . 7 | 8 | build: 9 | number: 0 10 | script: "{{ PYTHON }} -m pip install . --no-deps -vv" 11 | binary_relocation: False 12 | missing_dso_whitelist: "*" 13 | entry_points: 14 | - playwright = playwright.__main__:main 15 | 16 | requirements: 17 | build: 18 | - python >=3.9 # [build_platform != target_platform] 19 | - pip # [build_platform != target_platform] 20 | - cross-python_{{ target_platform }} # [build_platform != target_platform] 21 | host: 22 | - python >=3.9 23 | - wheel 24 | - pip 25 | - curl 26 | - setuptools_scm 27 | run: 28 | - python >=3.9 29 | # This should be the same as the dependencies in pyproject.toml 30 | - greenlet>=3.1.1,<4.0.0 31 | - pyee>=13,<14 32 | 33 | test: # [build_platform == target_platform] 34 | files: 35 | - scripts/example_sync.py 36 | - scripts/example_async.py 37 | requires: 38 | - pip 39 | imports: 40 | - playwright 41 | - playwright.sync_api 42 | - playwright.async_api 43 | commands: 44 | - playwright --help 45 | - playwright install --with-deps 46 | - python scripts/example_sync.py 47 | - python scripts/example_async.py 48 | 49 | about: 50 | home: https://github.com/microsoft/playwright-python 51 | license: Apache-2.0 52 | license_family: Apache 53 | license_file: LICENSE 54 | summary: Python version of the Playwright testing and automation library. 55 | description: | 56 | Playwright is a Python library to automate Chromium, 57 | Firefox and WebKit browsers with a single API. Playwright 58 | delivers automation that is ever-green, capable, reliable 59 | and fast. 60 | doc_url: https://playwright.dev/python/docs/intro/ 61 | dev_url: https://github.com/microsoft/playwright-python 62 | -------------------------------------------------------------------------------- /playwright/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | Python package `playwright` is a Python library to automate Chromium, 17 | Firefox and WebKit with a single API. Playwright is built to enable cross-browser 18 | web automation that is ever-green, capable, reliable and fast. 19 | """ 20 | -------------------------------------------------------------------------------- /playwright/__main__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import subprocess 16 | import sys 17 | 18 | from playwright._impl._driver import compute_driver_executable, get_driver_env 19 | 20 | 21 | def main() -> None: 22 | try: 23 | driver_executable, driver_cli = compute_driver_executable() 24 | completed_process = subprocess.run( 25 | [driver_executable, driver_cli, *sys.argv[1:]], env=get_driver_env() 26 | ) 27 | sys.exit(completed_process.returncode) 28 | except KeyboardInterrupt: 29 | sys.exit(130) 30 | 31 | 32 | if __name__ == "__main__": 33 | main() 34 | -------------------------------------------------------------------------------- /playwright/_impl/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/playwright/_impl/__init__.py -------------------------------------------------------------------------------- /playwright/_impl/__pyinstaller/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | from typing import List 17 | 18 | 19 | def get_hook_dirs() -> List[str]: 20 | return [os.path.dirname(__file__)] 21 | -------------------------------------------------------------------------------- /playwright/_impl/__pyinstaller/hook-playwright.async_api.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from PyInstaller.utils.hooks import collect_data_files # type: ignore 16 | 17 | datas = collect_data_files("playwright") 18 | -------------------------------------------------------------------------------- /playwright/_impl/__pyinstaller/hook-playwright.sync_api.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from PyInstaller.utils.hooks import collect_data_files # type: ignore 16 | 17 | datas = collect_data_files("playwright") 18 | -------------------------------------------------------------------------------- /playwright/_impl/_accessibility.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Dict, Optional 16 | 17 | from playwright._impl._connection import Channel 18 | from playwright._impl._element_handle import ElementHandle 19 | from playwright._impl._helper import locals_to_params 20 | 21 | 22 | def _ax_node_from_protocol(axNode: Dict) -> Dict: 23 | result = {**axNode} 24 | if "valueNumber" in axNode: 25 | result["value"] = axNode["valueNumber"] 26 | elif "valueString" in axNode: 27 | result["value"] = axNode["valueString"] 28 | 29 | if "checked" in axNode: 30 | result["checked"] = ( 31 | True 32 | if axNode.get("checked") == "checked" 33 | else ( 34 | False if axNode.get("checked") == "unchecked" else axNode.get("checked") 35 | ) 36 | ) 37 | 38 | if "pressed" in axNode: 39 | result["pressed"] = ( 40 | True 41 | if axNode.get("pressed") == "pressed" 42 | else ( 43 | False if axNode.get("pressed") == "released" else axNode.get("pressed") 44 | ) 45 | ) 46 | 47 | if axNode.get("children"): 48 | result["children"] = list(map(_ax_node_from_protocol, axNode["children"])) 49 | if "valueNumber" in result: 50 | del result["valueNumber"] 51 | if "valueString" in result: 52 | del result["valueString"] 53 | return result 54 | 55 | 56 | class Accessibility: 57 | def __init__(self, channel: Channel) -> None: 58 | self._channel = channel 59 | self._loop = channel._connection._loop 60 | self._dispatcher_fiber = channel._connection._dispatcher_fiber 61 | 62 | async def snapshot( 63 | self, interestingOnly: bool = None, root: ElementHandle = None 64 | ) -> Optional[Dict]: 65 | params = locals_to_params(locals()) 66 | if root: 67 | params["root"] = root._channel 68 | result = await self._channel.send("accessibilitySnapshot", params) 69 | return _ax_node_from_protocol(result) if result else None 70 | -------------------------------------------------------------------------------- /playwright/_impl/_artifact.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import pathlib 16 | from pathlib import Path 17 | from typing import Dict, Optional, Union, cast 18 | 19 | from playwright._impl._connection import ChannelOwner, from_channel 20 | from playwright._impl._helper import Error, make_dirs_for_file, patch_error_message 21 | from playwright._impl._stream import Stream 22 | 23 | 24 | class Artifact(ChannelOwner): 25 | def __init__( 26 | self, parent: ChannelOwner, type: str, guid: str, initializer: Dict 27 | ) -> None: 28 | super().__init__(parent, type, guid, initializer) 29 | self.absolute_path = initializer["absolutePath"] 30 | 31 | async def path_after_finished(self) -> pathlib.Path: 32 | if self._connection.is_remote: 33 | raise Error( 34 | "Path is not available when using browser_type.connect(). Use save_as() to save a local copy." 35 | ) 36 | path = await self._channel.send("pathAfterFinished") 37 | return pathlib.Path(path) 38 | 39 | async def save_as(self, path: Union[str, Path]) -> None: 40 | stream = cast(Stream, from_channel(await self._channel.send("saveAsStream"))) 41 | make_dirs_for_file(path) 42 | await stream.save_as(path) 43 | 44 | async def failure(self) -> Optional[str]: 45 | reason = await self._channel.send("failure") 46 | if reason is None: 47 | return None 48 | return patch_error_message(reason) 49 | 50 | async def delete(self) -> None: 51 | await self._channel.send("delete") 52 | 53 | async def read_info_buffer(self) -> bytes: 54 | stream = cast(Stream, from_channel(await self._channel.send("stream"))) 55 | buffer = await stream.read_all() 56 | return buffer 57 | 58 | async def cancel(self) -> None: # pyright: ignore[reportIncompatibleMethodOverride] 59 | await self._channel.send("cancel") 60 | -------------------------------------------------------------------------------- /playwright/_impl/_cdp_session.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Any, Dict 16 | 17 | from playwright._impl._connection import ChannelOwner 18 | from playwright._impl._helper import locals_to_params 19 | 20 | 21 | class CDPSession(ChannelOwner): 22 | def __init__( 23 | self, parent: ChannelOwner, type: str, guid: str, initializer: Dict 24 | ) -> None: 25 | super().__init__(parent, type, guid, initializer) 26 | self._channel.on("event", lambda params: self._on_event(params)) 27 | 28 | def _on_event(self, params: Any) -> None: 29 | self.emit(params["method"], params.get("params")) 30 | 31 | async def send(self, method: str, params: Dict = None) -> Dict: 32 | return await self._channel.send("send", locals_to_params(locals())) 33 | 34 | async def detach(self) -> None: 35 | await self._channel.send("detach") 36 | -------------------------------------------------------------------------------- /playwright/_impl/_console_message.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from asyncio import AbstractEventLoop 16 | from typing import TYPE_CHECKING, Any, Dict, List, Optional 17 | 18 | from playwright._impl._api_structures import SourceLocation 19 | from playwright._impl._connection import from_channel, from_nullable_channel 20 | from playwright._impl._js_handle import JSHandle 21 | 22 | if TYPE_CHECKING: # pragma: no cover 23 | from playwright._impl._page import Page 24 | 25 | 26 | class ConsoleMessage: 27 | def __init__( 28 | self, event: Dict, loop: AbstractEventLoop, dispatcher_fiber: Any 29 | ) -> None: 30 | self._event = event 31 | self._loop = loop 32 | self._dispatcher_fiber = dispatcher_fiber 33 | self._page: Optional["Page"] = from_nullable_channel(event.get("page")) 34 | 35 | def __repr__(self) -> str: 36 | return f"" 37 | 38 | def __str__(self) -> str: 39 | return self.text 40 | 41 | @property 42 | def type(self) -> str: 43 | return self._event["type"] 44 | 45 | @property 46 | def text(self) -> str: 47 | return self._event["text"] 48 | 49 | @property 50 | def args(self) -> List[JSHandle]: 51 | return list(map(from_channel, self._event["args"])) 52 | 53 | @property 54 | def location(self) -> SourceLocation: 55 | return self._event["location"] 56 | 57 | @property 58 | def page(self) -> Optional["Page"]: 59 | return self._page 60 | -------------------------------------------------------------------------------- /playwright/_impl/_dialog.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import TYPE_CHECKING, Dict, Optional 16 | 17 | from playwright._impl._connection import ChannelOwner, from_nullable_channel 18 | from playwright._impl._helper import locals_to_params 19 | 20 | if TYPE_CHECKING: # pragma: no cover 21 | from playwright._impl._page import Page 22 | 23 | 24 | class Dialog(ChannelOwner): 25 | def __init__( 26 | self, parent: ChannelOwner, type: str, guid: str, initializer: Dict 27 | ) -> None: 28 | super().__init__(parent, type, guid, initializer) 29 | self._page: Optional["Page"] = from_nullable_channel(initializer.get("page")) 30 | 31 | def __repr__(self) -> str: 32 | return f"" 33 | 34 | @property 35 | def type(self) -> str: 36 | return self._initializer["type"] 37 | 38 | @property 39 | def message(self) -> str: 40 | return self._initializer["message"] 41 | 42 | @property 43 | def default_value(self) -> str: 44 | return self._initializer["defaultValue"] 45 | 46 | @property 47 | def page(self) -> Optional["Page"]: 48 | return self._page 49 | 50 | async def accept(self, promptText: str = None) -> None: 51 | await self._channel.send("accept", locals_to_params(locals())) 52 | 53 | async def dismiss(self) -> None: 54 | await self._channel.send("dismiss") 55 | -------------------------------------------------------------------------------- /playwright/_impl/_download.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import pathlib 16 | from pathlib import Path 17 | from typing import TYPE_CHECKING, Optional, Union 18 | 19 | from playwright._impl._artifact import Artifact 20 | 21 | if TYPE_CHECKING: # pragma: no cover 22 | from playwright._impl._page import Page 23 | 24 | 25 | class Download: 26 | def __init__( 27 | self, page: "Page", url: str, suggested_filename: str, artifact: Artifact 28 | ) -> None: 29 | self._page = page 30 | self._loop = page._loop 31 | self._dispatcher_fiber = page._dispatcher_fiber 32 | self._url = url 33 | self._suggested_filename = suggested_filename 34 | self._artifact = artifact 35 | 36 | def __repr__(self) -> str: 37 | return f"" 38 | 39 | @property 40 | def page(self) -> "Page": 41 | return self._page 42 | 43 | @property 44 | def url(self) -> str: 45 | return self._url 46 | 47 | @property 48 | def suggested_filename(self) -> str: 49 | return self._suggested_filename 50 | 51 | async def delete(self) -> None: 52 | await self._artifact.delete() 53 | 54 | async def failure(self) -> Optional[str]: 55 | return await self._artifact.failure() 56 | 57 | async def path(self) -> pathlib.Path: 58 | return await self._artifact.path_after_finished() 59 | 60 | async def save_as(self, path: Union[str, Path]) -> None: 61 | await self._artifact.save_as(path) 62 | 63 | async def cancel(self) -> None: 64 | return await self._artifact.cancel() 65 | -------------------------------------------------------------------------------- /playwright/_impl/_driver.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import inspect 16 | import os 17 | import sys 18 | from pathlib import Path 19 | from typing import Tuple 20 | 21 | import playwright 22 | from playwright._repo_version import version 23 | 24 | 25 | def compute_driver_executable() -> Tuple[str, str]: 26 | driver_path = Path(inspect.getfile(playwright)).parent / "driver" 27 | cli_path = str(driver_path / "package" / "cli.js") 28 | if sys.platform == "win32": 29 | return ( 30 | os.getenv("PLAYWRIGHT_NODEJS_PATH", str(driver_path / "node.exe")), 31 | cli_path, 32 | ) 33 | return (os.getenv("PLAYWRIGHT_NODEJS_PATH", str(driver_path / "node")), cli_path) 34 | 35 | 36 | def get_driver_env() -> dict: 37 | env = os.environ.copy() 38 | env["PW_LANG_NAME"] = "python" 39 | env["PW_LANG_NAME_VERSION"] = f"{sys.version_info.major}.{sys.version_info.minor}" 40 | env["PW_CLI_DISPLAY_VERSION"] = version 41 | return env 42 | -------------------------------------------------------------------------------- /playwright/_impl/_errors.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # These are types that we use in the API. They are public and are a part of the 16 | # stable API. 17 | 18 | 19 | from typing import Optional 20 | 21 | 22 | def is_target_closed_error(error: Exception) -> bool: 23 | return isinstance(error, TargetClosedError) 24 | 25 | 26 | class Error(Exception): 27 | def __init__(self, message: str) -> None: 28 | self._message = message 29 | self._name: Optional[str] = None 30 | self._stack: Optional[str] = None 31 | super().__init__(message) 32 | 33 | @property 34 | def message(self) -> str: 35 | return self._message 36 | 37 | @property 38 | def name(self) -> Optional[str]: 39 | return self._name 40 | 41 | @property 42 | def stack(self) -> Optional[str]: 43 | return self._stack 44 | 45 | 46 | class TimeoutError(Error): 47 | pass 48 | 49 | 50 | class TargetClosedError(Error): 51 | def __init__(self, message: str = None) -> None: 52 | super().__init__(message or "Target page, context or browser has been closed") 53 | 54 | 55 | def rewrite_error(error: Exception, message: str) -> Exception: 56 | rewritten_exc = type(error)(message) 57 | if isinstance(rewritten_exc, Error) and isinstance(error, Error): 58 | rewritten_exc._name = error.name 59 | rewritten_exc._stack = error.stack 60 | return rewritten_exc 61 | -------------------------------------------------------------------------------- /playwright/_impl/_event_context_manager.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import asyncio 16 | from typing import Any, Generic, TypeVar 17 | 18 | T = TypeVar("T") 19 | 20 | 21 | class EventContextManagerImpl(Generic[T]): 22 | def __init__(self, future: asyncio.Future) -> None: 23 | self._future: asyncio.Future = future 24 | 25 | @property 26 | def future(self) -> asyncio.Future: 27 | return self._future 28 | 29 | async def __aenter__(self) -> asyncio.Future: 30 | return self._future 31 | 32 | async def __aexit__(self, *args: Any) -> None: 33 | await self._future 34 | -------------------------------------------------------------------------------- /playwright/_impl/_file_chooser.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from pathlib import Path 16 | from typing import TYPE_CHECKING, Sequence, Union 17 | 18 | from playwright._impl._api_structures import FilePayload 19 | 20 | if TYPE_CHECKING: # pragma: no cover 21 | from playwright._impl._element_handle import ElementHandle 22 | from playwright._impl._page import Page 23 | 24 | 25 | class FileChooser: 26 | def __init__( 27 | self, page: "Page", element_handle: "ElementHandle", is_multiple: bool 28 | ) -> None: 29 | self._page = page 30 | self._loop = page._loop 31 | self._dispatcher_fiber = page._dispatcher_fiber 32 | self._element_handle = element_handle 33 | self._is_multiple = is_multiple 34 | 35 | def __repr__(self) -> str: 36 | return f"" 37 | 38 | @property 39 | def page(self) -> "Page": 40 | return self._page 41 | 42 | @property 43 | def element(self) -> "ElementHandle": 44 | return self._element_handle 45 | 46 | def is_multiple(self) -> bool: 47 | return self._is_multiple 48 | 49 | async def set_files( 50 | self, 51 | files: Union[ 52 | str, Path, FilePayload, Sequence[Union[str, Path]], Sequence[FilePayload] 53 | ], 54 | timeout: float = None, 55 | noWaitAfter: bool = None, 56 | ) -> None: 57 | await self._element_handle.set_input_files(files, timeout, noWaitAfter) 58 | -------------------------------------------------------------------------------- /playwright/_impl/_glob.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping 16 | escaped_chars = {"$", "^", "+", ".", "*", "(", ")", "|", "\\", "?", "{", "}", "[", "]"} 17 | 18 | 19 | def glob_to_regex_pattern(glob: str) -> str: 20 | tokens = ["^"] 21 | in_group = False 22 | 23 | i = 0 24 | while i < len(glob): 25 | c = glob[i] 26 | if c == "\\" and i + 1 < len(glob): 27 | char = glob[i + 1] 28 | tokens.append("\\" + char if char in escaped_chars else char) 29 | i += 1 30 | elif c == "*": 31 | before_deep = glob[i - 1] if i > 0 else None 32 | star_count = 1 33 | while i + 1 < len(glob) and glob[i + 1] == "*": 34 | star_count += 1 35 | i += 1 36 | after_deep = glob[i + 1] if i + 1 < len(glob) else None 37 | is_deep = ( 38 | star_count > 1 39 | and (before_deep == "/" or before_deep is None) 40 | and (after_deep == "/" or after_deep is None) 41 | ) 42 | if is_deep: 43 | tokens.append("((?:[^/]*(?:/|$))*)") 44 | i += 1 45 | else: 46 | tokens.append("([^/]*)") 47 | else: 48 | if c == "{": 49 | in_group = True 50 | tokens.append("(") 51 | elif c == "}": 52 | in_group = False 53 | tokens.append(")") 54 | elif c == ",": 55 | if in_group: 56 | tokens.append("|") 57 | else: 58 | tokens.append("\\" + c) 59 | else: 60 | tokens.append("\\" + c if c in escaped_chars else c) 61 | i += 1 62 | 63 | tokens.append("$") 64 | return "".join(tokens) 65 | -------------------------------------------------------------------------------- /playwright/_impl/_greenlets.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import os 15 | from typing import Tuple 16 | 17 | import greenlet 18 | 19 | 20 | def _greenlet_trace_callback( 21 | event: str, args: Tuple[greenlet.greenlet, greenlet.greenlet] 22 | ) -> None: 23 | if event in ("switch", "throw"): 24 | origin, target = args 25 | print(f"Transfer from {origin} to {target} with {event}") 26 | 27 | 28 | if os.environ.get("INTERNAL_PW_GREENLET_DEBUG"): 29 | greenlet.settrace(_greenlet_trace_callback) 30 | 31 | 32 | class MainGreenlet(greenlet.greenlet): 33 | def __str__(self) -> str: 34 | return "" 35 | 36 | 37 | class RouteGreenlet(greenlet.greenlet): 38 | def __str__(self) -> str: 39 | return "" 40 | 41 | 42 | class LocatorHandlerGreenlet(greenlet.greenlet): 43 | def __str__(self) -> str: 44 | return "" 45 | 46 | 47 | class EventGreenlet(greenlet.greenlet): 48 | def __str__(self) -> str: 49 | return "" 50 | -------------------------------------------------------------------------------- /playwright/_impl/_map.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from typing import Dict, Generic, Tuple, TypeVar 15 | 16 | K = TypeVar("K") 17 | V = TypeVar("V") 18 | 19 | 20 | class Map(Generic[K, V]): 21 | def __init__(self) -> None: 22 | self._entries: Dict[int, Tuple[K, V]] = {} 23 | 24 | def __contains__(self, item: K) -> bool: 25 | return id(item) in self._entries 26 | 27 | def __setitem__(self, idx: K, value: V) -> None: 28 | self._entries[id(idx)] = (idx, value) 29 | 30 | def __getitem__(self, obj: K) -> V: 31 | return self._entries[id(obj)][1] 32 | -------------------------------------------------------------------------------- /playwright/_impl/_path_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import inspect 16 | from pathlib import Path 17 | from types import FrameType 18 | from typing import cast 19 | 20 | 21 | def get_file_dirname() -> Path: 22 | """Returns the callee (`__file__`) directory name""" 23 | frame = cast(FrameType, inspect.currentframe()).f_back 24 | module = inspect.getmodule(frame) 25 | assert module 26 | assert module.__file__ 27 | return Path(module.__file__).parent.absolute() 28 | -------------------------------------------------------------------------------- /playwright/_impl/_playwright.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Dict 16 | 17 | from playwright._impl._browser_type import BrowserType 18 | from playwright._impl._connection import ChannelOwner, from_channel 19 | from playwright._impl._fetch import APIRequest 20 | from playwright._impl._selectors import Selectors, SelectorsOwner 21 | 22 | 23 | class Playwright(ChannelOwner): 24 | devices: Dict 25 | selectors: Selectors 26 | chromium: BrowserType 27 | firefox: BrowserType 28 | webkit: BrowserType 29 | request: APIRequest 30 | 31 | def __init__( 32 | self, parent: ChannelOwner, type: str, guid: str, initializer: Dict 33 | ) -> None: 34 | super().__init__(parent, type, guid, initializer) 35 | self.request = APIRequest(self) 36 | self.chromium = from_channel(initializer["chromium"]) 37 | self.chromium._playwright = self 38 | self.firefox = from_channel(initializer["firefox"]) 39 | self.firefox._playwright = self 40 | self.webkit = from_channel(initializer["webkit"]) 41 | self.webkit._playwright = self 42 | 43 | self.selectors = Selectors(self._loop, self._dispatcher_fiber) 44 | selectors_owner: SelectorsOwner = from_channel(initializer["selectors"]) 45 | self.selectors._add_channel(selectors_owner) 46 | 47 | self._connection.on( 48 | "close", lambda: self.selectors._remove_channel(selectors_owner) 49 | ) 50 | self.devices = self._connection.local_utils.devices 51 | 52 | def __getitem__(self, value: str) -> "BrowserType": 53 | if value == "chromium": 54 | return self.chromium 55 | elif value == "firefox": 56 | return self.firefox 57 | elif value == "webkit": 58 | return self.webkit 59 | raise ValueError("Invalid browser " + value) 60 | 61 | def _set_selectors(self, selectors: Selectors) -> None: 62 | selectors_owner = from_channel(self._initializer["selectors"]) 63 | self.selectors._remove_channel(selectors_owner) 64 | self.selectors = selectors 65 | self.selectors._add_channel(selectors_owner) 66 | 67 | async def stop(self) -> None: 68 | pass 69 | -------------------------------------------------------------------------------- /playwright/_impl/_str_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import json 16 | import re 17 | from typing import Pattern, Union 18 | 19 | 20 | def escape_regex_flags(pattern: Pattern) -> str: 21 | flags = "" 22 | if pattern.flags != 0: 23 | flags = "" 24 | if (pattern.flags & int(re.IGNORECASE)) != 0: 25 | flags += "i" 26 | if (pattern.flags & int(re.DOTALL)) != 0: 27 | flags += "s" 28 | if (pattern.flags & int(re.MULTILINE)) != 0: 29 | flags += "m" 30 | assert ( 31 | pattern.flags 32 | & ~(int(re.MULTILINE) | int(re.IGNORECASE) | int(re.DOTALL) | int(re.UNICODE)) 33 | == 0 34 | ), "Unexpected re.Pattern flag, only MULTILINE, IGNORECASE and DOTALL are supported." 35 | return flags 36 | 37 | 38 | def escape_for_regex(text: str) -> str: 39 | return re.sub(r"[.*+?^>${}()|[\]\\]", "\\$&", text) 40 | 41 | 42 | def escape_regex_for_selector(text: Pattern) -> str: 43 | # Even number of backslashes followed by the quote -> insert a backslash. 44 | return ( 45 | "/" 46 | + re.sub(r'(^|[^\\])(\\\\)*(["\'`])', r"\1\2\\\3", text.pattern).replace( 47 | ">>", "\\>\\>" 48 | ) 49 | + "/" 50 | + escape_regex_flags(text) 51 | ) 52 | 53 | 54 | def escape_for_text_selector( 55 | text: Union[str, Pattern[str]], exact: bool = None, case_sensitive: bool = None 56 | ) -> str: 57 | if isinstance(text, Pattern): 58 | return escape_regex_for_selector(text) 59 | return json.dumps(text) + ("s" if exact else "i") 60 | 61 | 62 | def escape_for_attribute_selector( 63 | value: Union[str, Pattern], exact: bool = None 64 | ) -> str: 65 | if isinstance(value, Pattern): 66 | return escape_regex_for_selector(value) 67 | # TODO: this should actually be 68 | # cssEscape(value).replace(/\\ /g, ' ') 69 | # However, our attribute selectors do not conform to CSS parsing spec, 70 | # so we escape them differently. 71 | return ( 72 | '"' 73 | + value.replace("\\", "\\\\").replace('"', '\\"') 74 | + '"' 75 | + ("s" if exact else "i") 76 | ) 77 | -------------------------------------------------------------------------------- /playwright/_impl/_stream.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import base64 16 | from pathlib import Path 17 | from typing import Dict, Union 18 | 19 | from playwright._impl._connection import ChannelOwner 20 | 21 | 22 | class Stream(ChannelOwner): 23 | def __init__( 24 | self, parent: ChannelOwner, type: str, guid: str, initializer: Dict 25 | ) -> None: 26 | super().__init__(parent, type, guid, initializer) 27 | 28 | async def save_as(self, path: Union[str, Path]) -> None: 29 | file = await self._loop.run_in_executor(None, lambda: open(path, "wb")) 30 | while True: 31 | binary = await self._channel.send("read", {"size": 1024 * 1024}) 32 | if not binary: 33 | break 34 | await self._loop.run_in_executor( 35 | None, lambda: file.write(base64.b64decode(binary)) 36 | ) 37 | await self._loop.run_in_executor(None, lambda: file.close()) 38 | 39 | async def read_all(self) -> bytes: 40 | binary = b"" 41 | while True: 42 | chunk = await self._channel.send("read", {"size": 1024 * 1024}) 43 | if not chunk: 44 | break 45 | binary += base64.b64decode(chunk) 46 | return binary 47 | -------------------------------------------------------------------------------- /playwright/_impl/_web_error.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from asyncio import AbstractEventLoop 16 | from typing import Any, Optional 17 | 18 | from playwright._impl._helper import Error 19 | from playwright._impl._page import Page 20 | 21 | 22 | class WebError: 23 | def __init__( 24 | self, 25 | loop: AbstractEventLoop, 26 | dispatcher_fiber: Any, 27 | page: Optional[Page], 28 | error: Error, 29 | ) -> None: 30 | self._loop = loop 31 | self._dispatcher_fiber = dispatcher_fiber 32 | self._page = page 33 | self._error = error 34 | 35 | @property 36 | def page(self) -> Optional[Page]: 37 | return self._page 38 | 39 | @property 40 | def error(self) -> Error: 41 | return self._error 42 | -------------------------------------------------------------------------------- /playwright/_impl/_writable_stream.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import base64 16 | import os 17 | from pathlib import Path 18 | from typing import Dict, Union 19 | 20 | from playwright._impl._connection import ChannelOwner 21 | 22 | # COPY_BUFSIZE is taken from shutil.py in the standard library 23 | _WINDOWS = os.name == "nt" 24 | COPY_BUFSIZE = 1024 * 1024 if _WINDOWS else 64 * 1024 25 | 26 | 27 | class WritableStream(ChannelOwner): 28 | def __init__( 29 | self, parent: ChannelOwner, type: str, guid: str, initializer: Dict 30 | ) -> None: 31 | super().__init__(parent, type, guid, initializer) 32 | 33 | async def copy(self, path: Union[str, Path]) -> None: 34 | with open(path, "rb") as f: 35 | while True: 36 | data = f.read(COPY_BUFSIZE) 37 | if not data: 38 | break 39 | await self._channel.send( 40 | "write", {"binary": base64.b64encode(data).decode()} 41 | ) 42 | await self._channel.send("close") 43 | -------------------------------------------------------------------------------- /playwright/async_api/_context_manager.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import asyncio 16 | from typing import Any 17 | 18 | from playwright._impl._connection import Connection 19 | from playwright._impl._object_factory import create_remote_object 20 | from playwright._impl._transport import PipeTransport 21 | from playwright.async_api._generated import Playwright as AsyncPlaywright 22 | 23 | 24 | class PlaywrightContextManager: 25 | def __init__(self) -> None: 26 | self._connection: Connection 27 | self._exit_was_called = False 28 | 29 | async def __aenter__(self) -> AsyncPlaywright: 30 | loop = asyncio.get_running_loop() 31 | self._connection = Connection( 32 | None, 33 | create_remote_object, 34 | PipeTransport(loop), 35 | loop, 36 | ) 37 | loop.create_task(self._connection.run()) 38 | playwright_future = self._connection.playwright_future 39 | 40 | done, _ = await asyncio.wait( 41 | {self._connection._transport.on_error_future, playwright_future}, 42 | return_when=asyncio.FIRST_COMPLETED, 43 | ) 44 | if not playwright_future.done(): 45 | playwright_future.cancel() 46 | playwright = AsyncPlaywright(next(iter(done)).result()) 47 | playwright.stop = self.__aexit__ # type: ignore 48 | return playwright 49 | 50 | async def start(self) -> AsyncPlaywright: 51 | return await self.__aenter__() 52 | 53 | async def __aexit__(self, *args: Any) -> None: 54 | if self._exit_was_called: 55 | return 56 | self._exit_was_called = True 57 | await self._connection.stop_async() 58 | -------------------------------------------------------------------------------- /playwright/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/playwright/py.typed -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv pip compile pyproject.toml -o requirements.txt 3 | greenlet==3.2.2 4 | # via playwright (pyproject.toml) 5 | pyee==13.0.0 6 | # via playwright (pyproject.toml) 7 | typing-extensions==4.13.2 8 | # via pyee 9 | -------------------------------------------------------------------------------- /scripts/example_async.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import asyncio 16 | 17 | from playwright.async_api import async_playwright 18 | 19 | 20 | async def main() -> None: 21 | async with async_playwright() as p: 22 | for browser_type in [p.chromium, p.firefox, p.webkit]: 23 | browser = await browser_type.launch() 24 | page = await browser.new_page() 25 | assert await page.evaluate("() => 11 * 11") == 121 26 | await browser.close() 27 | 28 | 29 | if __name__ == "__main__": 30 | asyncio.run(main()) 31 | -------------------------------------------------------------------------------- /scripts/example_sync.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from playwright.sync_api import sync_playwright 16 | 17 | 18 | def main() -> None: 19 | with sync_playwright() as p: 20 | for browser_type in [p.chromium, p.firefox, p.webkit]: 21 | browser = browser_type.launch() 22 | page = browser.new_page() 23 | assert page.evaluate("() => 11 * 11") == 121 24 | browser.close() 25 | 26 | 27 | if __name__ == "__main__": 28 | main() 29 | -------------------------------------------------------------------------------- /scripts/expected_api_mismatch.txt: -------------------------------------------------------------------------------- 1 | # Playwright Python API 2 | 3 | # Hidden property 4 | Parameter not documented: Browser.new_context(default_browser_type=) 5 | Parameter not documented: Browser.new_page(default_browser_type=) 6 | 7 | # We don't expand the type of the return value here. 8 | 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] 9 | 10 | # One vs two arguments in the callback, Python explicitly unions. 11 | 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]] 12 | 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] 13 | Parameter type mismatch in Page.route(handler=): documented as Callable[[Route, Request], Union[Any, Any]], code has Union[Callable[[Route, Request], Any], Callable[[Route], Any]] 14 | Parameter type mismatch in Page.unroute(handler=): documented as Union[Callable[[Route, Request], Union[Any, Any]], None], code has Union[Callable[[Route, Request], Any], Callable[[Route], Any], None] 15 | 16 | # One vs two arguments in the callback, Python explicitly unions. 17 | Parameter type mismatch in Page.add_locator_handler(handler=): documented as Callable[[Locator], Any], code has Union[Callable[[Locator], Any], Callable[[], Any]] 18 | 19 | Parameter type mismatch in BrowserContext.route_web_socket(handler=): documented as Callable[[WebSocketRoute], Union[Any, Any]], code has Callable[[WebSocketRoute], Any] 20 | Parameter type mismatch in Page.route_web_socket(handler=): documented as Callable[[WebSocketRoute], Union[Any, Any]], code has Callable[[WebSocketRoute], Any] 21 | Parameter type mismatch in WebSocketRoute.on_close(handler=): documented as Callable[[Union[int, undefined]], Union[Any, Any]], code has Callable[[Union[int, None], Union[str, None]], Any] 22 | Parameter type mismatch in WebSocketRoute.on_message(handler=): documented as Callable[[str], Union[Any, Any]], code has Callable[[Union[bytes, str]], Any] 23 | -------------------------------------------------------------------------------- /scripts/update_api.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function update_api { 4 | echo "Generating $1" 5 | file_name="$1" 6 | generate_script="$2" 7 | git checkout HEAD -- "$file_name" 8 | 9 | if PYTHONIOENCODING=utf-8 python "$generate_script" > .x; then 10 | mv .x "$file_name" 11 | pre-commit run --files $file_name 12 | echo "Regenerated APIs" 13 | else 14 | echo "Exited due to errors" 15 | exit 1 16 | fi 17 | } 18 | 19 | update_api "playwright/sync_api/_generated.py" "scripts/generate_sync_api.py" 20 | update_api "playwright/async_api/_generated.py" "scripts/generate_async_api.py" 21 | 22 | playwright install 23 | 24 | python scripts/update_versions.py 25 | -------------------------------------------------------------------------------- /scripts/update_versions.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | import re 15 | from pathlib import Path 16 | 17 | from playwright.sync_api import sync_playwright 18 | 19 | 20 | def main() -> None: 21 | with sync_playwright() as p: 22 | readme = Path("README.md").resolve() 23 | text = readme.read_text(encoding="utf-8") 24 | for browser_type in [p.chromium, p.firefox, p.webkit]: 25 | rx = re.compile( 26 | r"([^<]+)" 29 | ) 30 | browser = browser_type.launch() 31 | text = rx.sub( 32 | f"{browser.version}", 33 | text, 34 | ) 35 | browser.close() 36 | readme.write_text(text, encoding="utf-8") 37 | 38 | 39 | if __name__ == "__main__": 40 | main() 41 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | E501 4 | W503 5 | E302 6 | # Conflicts with black https://github.com/PyCQA/flake8/issues/1921 7 | E704 8 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/__init__.py -------------------------------------------------------------------------------- /tests/assets/beforeunload.html: -------------------------------------------------------------------------------- 1 |
beforeunload demo.
2 | 10 | -------------------------------------------------------------------------------- /tests/assets/client-certificates/README.md: -------------------------------------------------------------------------------- 1 | # Client Certificate test-certificates 2 | 3 | ## Server 4 | 5 | ```bash 6 | openssl req \ 7 | -x509 \ 8 | -newkey rsa:4096 \ 9 | -keyout server/server_key.pem \ 10 | -out server/server_cert.pem \ 11 | -nodes \ 12 | -days 365 \ 13 | -subj "/CN=localhost/O=Client\ Certificate\ Demo" \ 14 | -addext "subjectAltName=DNS:localhost,DNS:local.playwright" 15 | ``` 16 | 17 | ## Trusted client-certificate (server signed/valid) 18 | 19 | ``` 20 | mkdir -p client/trusted 21 | # generate server-signed (valid) certifcate 22 | openssl req \ 23 | -newkey rsa:4096 \ 24 | -keyout client/trusted/key.pem \ 25 | -out client/trusted/csr.pem \ 26 | -nodes \ 27 | -days 365 \ 28 | -subj "/CN=Alice" 29 | 30 | # sign with server_cert.pem 31 | openssl x509 \ 32 | -req \ 33 | -in client/trusted/csr.pem \ 34 | -CA server/server_cert.pem \ 35 | -CAkey server/server_key.pem \ 36 | -out client/trusted/cert.pem \ 37 | -set_serial 01 \ 38 | -days 365 39 | ``` 40 | 41 | ## Self-signed certificate (invalid) 42 | 43 | ``` 44 | mkdir -p client/self-signed 45 | openssl req \ 46 | -newkey rsa:4096 \ 47 | -keyout client/self-signed/key.pem \ 48 | -out client/self-signed/csr.pem \ 49 | -nodes \ 50 | -days 365 \ 51 | -subj "/CN=Bob" 52 | 53 | # sign with self-signed/key.pem 54 | openssl x509 \ 55 | -req \ 56 | -in client/self-signed/csr.pem \ 57 | -signkey client/self-signed/key.pem \ 58 | -out client/self-signed/cert.pem \ 59 | -days 365 60 | ``` 61 | -------------------------------------------------------------------------------- /tests/assets/client-certificates/client/self-signed/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEyzCCArOgAwIBAgIUYps4gh4MqFYg8zqQhHYL7zYfbLkwDQYJKoZIhvcNAQEL 3 | BQAwDjEMMAoGA1UEAwwDQm9iMB4XDTI0MDcxOTEyNDc0MFoXDTI1MDcxOTEyNDc0 4 | MFowDjEMMAoGA1UEAwwDQm9iMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC 5 | AgEA179eTsqcc1c3AOQHzCZEyYLPta2CCAscUFqcEZ9vWvjW0uzOv9TDlB33Unov 6 | jch4CElZOBhzTadVsbmnYKpxwyVU89WCuQKvedz4k1vu7S1YryfNbmS8PWbnQ4ds 7 | 9NB7SgJNHZILvx9DXuWeFEyzRIo1984z4HheBzrkf791LqpYKaKziANUo8h8t0dm 8 | TX/boOz8cEnQNwtTC0ZX3aD0obG/UAhr/22ZGPo/E659fh4ptyYX2LrIUHGy+Eux 9 | nJ9Y4cTqa88Ee6K6AkDiT/AoNQNxE4X++jqLuie8j/ZYpI1Oll38GwKVOyy1msRL 10 | toGmISNwkMIQDGABrJlxgpP4QQAQ+08v9srzXOlkdxdr7OCP81r+ccBXiSQEe7BA 11 | kdJ8l98l5dprJ++GJ+SZcV4+/iGR0dKU2IdAG5HiKZIFn6ch9Ux+UMqeGaYCpkHr 12 | TiietHwcXgtVBlE0jFmB/HspmI/O0abK+grMmueaH7XtTI8YHnw0mUpL8+yp7mfA 13 | 7zFusgFgyiBPXeD/NQgg8vja67k++d1VGoXm2xr+5WPQCSbgQoMkkOBMLHWJTefd 14 | 6F4Z5M+oI0VwYbf6eQW246wJgpCHSPR0Vdijd6MAGRWKUuLfDsA9+12iGbKvwJ2e 15 | nJlStft2V2LZcjBfdIMbigW1aSVNN5w6m6YVrQPry3WPkWcCAwEAAaMhMB8wHQYD 16 | VR0OBBYEFPxKWTFQJSg4HD2qjxL0dnXX/z4qMA0GCSqGSIb3DQEBCwUAA4ICAQBz 17 | 4H1d5eGRU9bekUvi7LbZ5CP/I6w6PL/9AlXqO3BZKxplK7fYGHd3uqyDorJEsvjV 18 | hxwvFlEnS0JIU3nRzhJU/h4Yaivf1WLRFwGZ4TPBjX9KFU27exFWD3rppazkWybJ 19 | i4WuEdP3TJMdKLcNTtXWUDroDOgPlS66u6oZ+mUyUROil+B+fgQgVDhjRc5fvRgZ 20 | Lng8wuejCo3ExQyxkwn2G5guyIimgHmOQghPtLO5xlc67Z4GPUZ1m4tC+BCiFO4D 21 | YIXl3QiIpmU7Pss39LLKMGXXAgLRqyMzqE52lsznu18v5vDLfTaRH4u/wjzULhXz 22 | SrV1IUJmhgEXta4EeDmPH0itgKtkbwjgCOD7drrFrJq/EnvIaJ5cpxiI1pFmYD8g 23 | VVD7/KT/CyT1Uz1dI8QaP/JX8XEgtMJaSkPfjPErIViN9rh9ECCNLgFyv7Y0Plar 24 | A6YlvdyV1Rta/BHndf5Hqz9QWNhbFCMQRGVQNEcoKwpFyjAE9SXoKJvFIK/w5WXu 25 | qKzIYA26QXE3p734Xu1n8QiFJIyltVHbyUlD0k06194t5a2WK+/eDeReIsk0QOI8 26 | FGqhyPZ7YjR5tSZTmgljtViqBO5AA23QOVFqtjOUrjXP5pTbPJel99Z/FTkqSwvB 27 | Rt4OX7HfuokWQDTT0TMn5jVtJyi54cH7f9MmsNJ23g== 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /tests/assets/client-certificates/client/self-signed/csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIEUzCCAjsCAQAwDjEMMAoGA1UEAwwDQm9iMIICIjANBgkqhkiG9w0BAQEFAAOC 3 | Ag8AMIICCgKCAgEA179eTsqcc1c3AOQHzCZEyYLPta2CCAscUFqcEZ9vWvjW0uzO 4 | v9TDlB33Unovjch4CElZOBhzTadVsbmnYKpxwyVU89WCuQKvedz4k1vu7S1YryfN 5 | bmS8PWbnQ4ds9NB7SgJNHZILvx9DXuWeFEyzRIo1984z4HheBzrkf791LqpYKaKz 6 | iANUo8h8t0dmTX/boOz8cEnQNwtTC0ZX3aD0obG/UAhr/22ZGPo/E659fh4ptyYX 7 | 2LrIUHGy+EuxnJ9Y4cTqa88Ee6K6AkDiT/AoNQNxE4X++jqLuie8j/ZYpI1Oll38 8 | GwKVOyy1msRLtoGmISNwkMIQDGABrJlxgpP4QQAQ+08v9srzXOlkdxdr7OCP81r+ 9 | ccBXiSQEe7BAkdJ8l98l5dprJ++GJ+SZcV4+/iGR0dKU2IdAG5HiKZIFn6ch9Ux+ 10 | UMqeGaYCpkHrTiietHwcXgtVBlE0jFmB/HspmI/O0abK+grMmueaH7XtTI8YHnw0 11 | mUpL8+yp7mfA7zFusgFgyiBPXeD/NQgg8vja67k++d1VGoXm2xr+5WPQCSbgQoMk 12 | kOBMLHWJTefd6F4Z5M+oI0VwYbf6eQW246wJgpCHSPR0Vdijd6MAGRWKUuLfDsA9 13 | +12iGbKvwJ2enJlStft2V2LZcjBfdIMbigW1aSVNN5w6m6YVrQPry3WPkWcCAwEA 14 | AaAAMA0GCSqGSIb3DQEBCwUAA4ICAQCb07d2IjUy1PeHCj/2k/z9FrZSo6K3c8y6 15 | b/u/MZ0AXPKLPDSo7UYpOJ8Z2cBiJ8jQapjTSEL8POUYqcvCmP55R6u68KmvINHo 16 | +Ly7pP+xPrbA4Q0WmPnz37hQn+I1he0GuEQyjZZqUln9zwp67TsWNKxKtCH+1j8M 17 | Ltzx6kuHCdPtDUtv291yhVRqvbjiDs+gzdQYNJtAkUbHwHFxu8oZhg8QZGyXYMN8 18 | TGoQ1LTezFZXJtX69K7WnrDGrjsgB6EMvwkqAFSYNH0LFvI0xo13OOgXr9mrwohA 19 | 76uZtjXL9B15EqrMce6mdUZi46QJuQ2avTi57Lz+fqvsBYdQO89VcFSmqu2nfspN 20 | QZDrooyjHrlls8MpoBd8fde9oT4uA4/d9SJtuHUnjgGN7Qr7eTruWXL8wVMwFnvL 21 | igWE4detO9y2gpRLq6uEqzWYMGtN9PXJCGU8C8m9E2EBUKMrT/bpNbboatLcgRrW 22 | acj0BRVqoVzk1sRq7Sa6ejywqgARvIhTehg6DqdMdcENCPQ7rxDRu5PSDM8/mwIj 23 | 0KYl8d2PlECB4ofRyLcy17BZzjP6hSnkGzcFk0/bChZOSIRnwvKbvfXnB45hhPk8 24 | XwT/6UNSwC2STP3gtOmLqrWj+OE0gy0AkDMvP3UnQVGMUvgfYg+N4ROCVtlqzxe9 25 | W65c05Mm1g== 26 | -----END CERTIFICATE REQUEST----- 27 | -------------------------------------------------------------------------------- /tests/assets/client-certificates/client/trusted/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFAzCCAuugAwIBAgIBATANBgkqhkiG9w0BAQsFADA2MRIwEAYDVQQDDAlsb2Nh 3 | bGhvc3QxIDAeBgNVBAoMF0NsaWVudCBDZXJ0aWZpY2F0ZSBEZW1vMB4XDTI0MDcx 4 | OTEyNDczN1oXDTI1MDcxOTEyNDczN1owEDEOMAwGA1UEAwwFQWxpY2UwggIiMA0G 5 | CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCac3+4rNmH4/N1s4HqR2X168tgS/aA 6 | 6sHW5at8mWRnq54Nm11RvnK55jHQYVAdBgJy5M07w0wakp8inxzlY95wqxBimYG6 7 | 3Un/1p7mX9FkB4LNISCc6j/s/Ufv85MXPbn0S5rm9UcQO9cINJb1RP1YgDDLN5cx 8 | Mz6X4nyofN8H6Lhvh4JDdBw4DfDEFERkVfF+bkZ7YW4XHEChgzm3RxCF0eeGzIXG 9 | rkkK9AsSdJAhOvTlHPFCQKXTYZhsL5+3Ma4RnWnDWvLTHx6KzoU+twTM2mYhhQuQ 10 | gQpnmDHxGge8kGeHGtfdgAjtVJTE57xF/shP0JU+tuIV8NNhQ/vEmhL0Wa093/Ev 11 | pTVp0EUEuDh9ORRH5K5M4bKJyU4XX5noiht6yOn00uaoJcWduUAWsU+cDSvDTMw8 12 | 1opWWm0QIAV3G2yuRSkumHAKqvQLeyeyiKz+OEhyEiZ7EZNExPD0TSpApSTU6aCT 13 | UAvPYGQ59VjsMHTuJ9r4wKIYaDvfL+t72vg2vTQma5cTOBJfIdxH9blFTjEnToH3 14 | LX8t0XndQ2RkiRnIze2p2jUShxo/lWCjCw+2Iaw0A0fNUK1BbOrFRPq1u7AnEuMJ 15 | t7HF50MloItM97R9vofDwgDIzlX/PzlVRcn1WCo8Fr/0EXxPPreX0YDIp1ANQ8fS 16 | v7bKb2vQIxWuCQIDAQABo0IwQDAdBgNVHQ4EFgQUVJVRJJ2k/Z4r0M1AXe6agyD4 17 | uCwwHwYDVR0jBBgwFoAUEHtrxWCk96Ehr60E0HBuwLk2i+IwDQYJKoZIhvcNAQEL 18 | BQADggIBAGEvSkxhxRKmlvKG8wCXop2OaUUAOG16+T96vd+aFYaJNlfGoPvqv4Lw 19 | qaHztVktnRrJ//fpNWOsdxkE1uPU4uyGjl2KbyH81JvkE6A3OX0P4B01n8lcimY2 20 | j3oje6KjORUouYVsypD1VcwfWJgsE3U2Txv5srD8BoemVWgWbWjfyim4kk8C5zlf 21 | tWEazVAaI4MWecqtU4P5gIEomCI7MG9ebxYp5oQhRxeOndOYdUbSzAkZj50gXFA1 22 | +TNkvuhTFlJF0F7qIFVJSJTmJ+6E5B4ddbkyUYwbOdO+P8mz5N5mSljE+EiIQTxo 23 | AwbG8cSivMy/jI3h048tCUONAJzcSWCF4k1r9Qr6xbyW2ud2GmKiFCEYJkYTsMWV 24 | fM/RujTHlGvJ2+bQK5HiNyW0tO9znW9kaoxolu1YBvTh2492v3agK7nALyGGgdo1 25 | /nN/ikgkQiyaCpZwFeooJv1YFU5aDhR9RjIIJ9UbJ8FdAv8Xd00E3viunLTvqqXK 26 | RVMokw+tFQTEzjKofKWYArPDjB9LUbN+vQbumKalis3+NlJ3WolYPrCg55tqt1o3 27 | zXi+xv7120cJFouilRFwrafNFV6F+pRMkMmiWopMnoVJPVXcoqyJRcsmO62uslhg 28 | BLFgAH4H/14drYrgWIMz0no78RInEz0z507zwLkWk5d9W9pJ/4Rf 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /tests/assets/client-certificates/client/trusted/csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIEVTCCAj0CAQAwEDEOMAwGA1UEAwwFQWxpY2UwggIiMA0GCSqGSIb3DQEBAQUA 3 | A4ICDwAwggIKAoICAQCac3+4rNmH4/N1s4HqR2X168tgS/aA6sHW5at8mWRnq54N 4 | m11RvnK55jHQYVAdBgJy5M07w0wakp8inxzlY95wqxBimYG63Un/1p7mX9FkB4LN 5 | ISCc6j/s/Ufv85MXPbn0S5rm9UcQO9cINJb1RP1YgDDLN5cxMz6X4nyofN8H6Lhv 6 | h4JDdBw4DfDEFERkVfF+bkZ7YW4XHEChgzm3RxCF0eeGzIXGrkkK9AsSdJAhOvTl 7 | HPFCQKXTYZhsL5+3Ma4RnWnDWvLTHx6KzoU+twTM2mYhhQuQgQpnmDHxGge8kGeH 8 | GtfdgAjtVJTE57xF/shP0JU+tuIV8NNhQ/vEmhL0Wa093/EvpTVp0EUEuDh9ORRH 9 | 5K5M4bKJyU4XX5noiht6yOn00uaoJcWduUAWsU+cDSvDTMw81opWWm0QIAV3G2yu 10 | RSkumHAKqvQLeyeyiKz+OEhyEiZ7EZNExPD0TSpApSTU6aCTUAvPYGQ59VjsMHTu 11 | J9r4wKIYaDvfL+t72vg2vTQma5cTOBJfIdxH9blFTjEnToH3LX8t0XndQ2RkiRnI 12 | ze2p2jUShxo/lWCjCw+2Iaw0A0fNUK1BbOrFRPq1u7AnEuMJt7HF50MloItM97R9 13 | vofDwgDIzlX/PzlVRcn1WCo8Fr/0EXxPPreX0YDIp1ANQ8fSv7bKb2vQIxWuCQID 14 | AQABoAAwDQYJKoZIhvcNAQELBQADggIBAGgf3EC8WL3RGmuGA+d/4wd1jNfrfU6n 15 | xjnDwdEEX0TQZGGPjh5xvoCK76yZPkO6+z0IYSepEmWBS27HJKl7nuoOvS7MjQyJ 16 | C+3Bdk3ToCeQjmNBlRBKsUw5ftTU902oMl5BptHGj1KGjYBLAkPdXb44wXSVKJ8q 17 | ihFhWlovsva6GDoUorksU3vOwijdlGzTANQHJGFncgrRud9ATavpGS3KVxR73R3A 18 | aBbu3Qw+QIfu8Qx5eBJp8CbMrpAmjfuq17STvqr5bC10Fnn4NegrnHOQG9JcK02+ 19 | 5Bn3+9X/n1mue7aohIdErLEiDMSqMOwFfrJeaH6YM1G4QkWyqGugtmHsWOUf0nlU 20 | nkH1krvfw9rb6b+03c4A6GSeHnbX5ufFDSf5gaR6Wy7c0jBnoxVbtBLH2zXlrd0k 21 | iRQG7C6XZzGMS7hb7GL7+bkRy9kWjmDL7z7Fp+EgzKhNmzuWII3E9X9va33HoQ/Q 22 | UdK3JVToxRQg6XRKOxL9+U/+8i6U8lxObLWkWh2cypZqbz5qJxa+2u5JYO/KEoHZ 23 | G963UX7XWezR98vZuTc1XHGZtBDMrjjDd7Kmb4/i/xBPeWwseeGtzFy9z2pnEnkL 24 | uKE4C8wUNpzUUlsn4LneZXObIoErE7FqAAlVFujVe7iaJBmXoUXZR36drbfiaODK 25 | vwAGyrYHaOlR 26 | -----END CERTIFICATE REQUEST----- 27 | -------------------------------------------------------------------------------- /tests/assets/client-certificates/server/server_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFdTCCA12gAwIBAgIUNPWupe2xcu8YYG1ozoqk9viqDJswDQYJKoZIhvcNAQEL 3 | BQAwNjESMBAGA1UEAwwJbG9jYWxob3N0MSAwHgYDVQQKDBdDbGllbnQgQ2VydGlm 4 | aWNhdGUgRGVtbzAeFw0yNDA3MTkxMjQ3MzNaFw0yNTA3MTkxMjQ3MzNaMDYxEjAQ 5 | BgNVBAMMCWxvY2FsaG9zdDEgMB4GA1UECgwXQ2xpZW50IENlcnRpZmljYXRlIERl 6 | bW8wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC+K5JWhlfvI47ZL/Az 7 | L0xnOl+cMelr2BqH+7XS8187SbvluhFfFkq/7V7rwgsHI64sn8pgRCOnqKWV6jtb 8 | 651dGzn7Nby6InmyOQzF4VwfSVWQ6BYXgXuryS9Gm0gi8sOL1Ji/jV49n1gzLyIx 9 | LNhd7NG2DCCedTHJnxyz4xq8MWhI/qI85iWJqcHhxkDb8wtH1Vd6nd/ZRVDbjgTv 10 | PH3EDK7JqmnYG9+x4Jz0yEhvV7jL3gNu2mIyttvm7oRna9oHgaKFUJt4BCfPbT5U 11 | 3ipvcq29hdD5/5QIDzTWcExTnklolg5xpFext1+3KPSppESxcfBBNoL3h1B8ZcZa 12 | lEMC/IoFUIDJQj5gmSn4okwMWIxgf+AL0609MKEqQ2FavOsvBmhHcQsqLk4MO/v0 13 | NGFv1/xGe4tUkX4han6ykf1+sqzupJT5qnUONmvghb2SpIt83o4j4KHVzZwk8JK0 14 | N6hN7JEjXQwSKCh3b0FFg+kPAe12d6BBcsNzEYmt2C1KNPbXMX84zIkgPN01XMg6 15 | kdCdjP6DH7CK+brW9qQufOqYpd3eNhJyeBm+oP3PhnhEiMTIO8X2GdSN5Rxozgxl 16 | VIj/QWhLV64r5AqPr/Vpd1vcsxrg3aS5CASmoWQmTPuhEZptRtrkPkGw7k9NPZ34 17 | lnRenvKJ9e3DXhXRMqeYUY6wjwIDAQABo3sweTAdBgNVHQ4EFgQUEHtrxWCk96Eh 18 | r60E0HBuwLk2i+IwHwYDVR0jBBgwFoAUEHtrxWCk96Ehr60E0HBuwLk2i+IwDwYD 19 | VR0TAQH/BAUwAwEB/zAmBgNVHREEHzAdgglsb2NhbGhvc3SCEGxvY2FsLnBsYXl3 20 | cmlnaHQwDQYJKoZIhvcNAQELBQADggIBALP4kOAP21ZusbEH89VkZT3MkGlZuDQP 21 | LyTYdLzT3EzN//2+lBDmJfpIPLL/K3sNEVSzNppa6tcCXiVNes/xJM7tHRhTOJ31 22 | HinSsib2r6DZ6SitQJWmD5FoAdkp9qdG8mA/5vOiwiVKKFV2/Z3i+3iUI/ZnEhUq 23 | uUA1I3TI5LAQzgWLwYu1jSEM1EbH6uQiZ8AmXLVO4GQnVQdbyarWHxIy+zsg+MJN 24 | fxIG/phDpkt1mI3SkAdpWRWjCKESQhrIcRUtu5eVk0lho6ttHODXF8bM7iWLoRc7 25 | rpcllI4HXHoXQqQkZHRa7KwTf0YVwwQbXTecZONWXwE9Ej5R5IcZzja5FWCSstsb 26 | ULNW0JVxGBE7j5aOjxasYAbRexDmlfEdLvnp6bctZuvMvuBxrB+x5HSEZl6bVnbC 27 | nvtoslylQJM1bwlZdCqJm04JXe1787HDBef2gABv27BjvG/zn89L5ipogZCrGpl6 28 | P9qs0eSERHuSrm3eHUVgXSQ1nbvOpk7RPFbsbp/npc1NbEDBdAMoXhLP9A+ytxLq 29 | TF+w08nfCF6yJJ3jTkvABo10UH6zcPnfH3Ys7JYsHRbcloMfn+mc88KrTaCO+VZx 30 | qjhFcz+zDu/AbtJkDJtxX2X7jNL0pzWS+9H8jFTrd3ta8XrJiSFq2VMxEU6R0IHk 31 | 2Ct10prMWB/3 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /tests/assets/client.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import sys 16 | from pathlib import Path 17 | 18 | from playwright.sync_api import Playwright, sync_playwright 19 | 20 | 21 | def main(playwright: Playwright, browser_name: str) -> None: 22 | browser = playwright[browser_name].launch() 23 | page = browser.new_page() 24 | page.goto("data:text/html,Foobar") 25 | here = Path(__file__).parent.resolve() 26 | page.screenshot(path=here / f"{browser_name}.png") 27 | page.close() 28 | browser.close() 29 | 30 | 31 | if __name__ == "__main__": 32 | browser_name = sys.argv[1] 33 | with sync_playwright() as p: 34 | main(p, browser_name) 35 | -------------------------------------------------------------------------------- /tests/assets/consolelog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | console.log test 5 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/assets/csp.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/assets/digits/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/assets/digits/0.png -------------------------------------------------------------------------------- /tests/assets/digits/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/assets/digits/1.png -------------------------------------------------------------------------------- /tests/assets/digits/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/assets/digits/2.png -------------------------------------------------------------------------------- /tests/assets/digits/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/assets/digits/3.png -------------------------------------------------------------------------------- /tests/assets/digits/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/assets/digits/4.png -------------------------------------------------------------------------------- /tests/assets/digits/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/assets/digits/5.png -------------------------------------------------------------------------------- /tests/assets/digits/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/assets/digits/6.png -------------------------------------------------------------------------------- /tests/assets/digits/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/assets/digits/7.png -------------------------------------------------------------------------------- /tests/assets/digits/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/assets/digits/8.png -------------------------------------------------------------------------------- /tests/assets/digits/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/assets/digits/9.png -------------------------------------------------------------------------------- /tests/assets/dom.html: -------------------------------------------------------------------------------- 1 |
Text, 2 | more text
3 | 4 | 5 | -------------------------------------------------------------------------------- /tests/assets/download-blob.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Blob Download Example 5 | 6 | 7 | 27 | Download 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/assets/drag-n-drop.html: -------------------------------------------------------------------------------- 1 | 14 | 15 | 33 | 34 | 35 |
36 |

37 | Select this element, drag it to the Drop Zone and then release the selection to move the element.

38 |
39 |
Drop Zone
40 | 41 | -------------------------------------------------------------------------------- /tests/assets/dummy_bad_browser_executable.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | process.exit(1); 4 | -------------------------------------------------------------------------------- /tests/assets/empty.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/assets/empty.html -------------------------------------------------------------------------------- /tests/assets/error.html: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /tests/assets/es6/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "sourceType": "module" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/assets/es6/es6import.js: -------------------------------------------------------------------------------- 1 | import num from './es6module.js'; 2 | window.__es6injected = num; 3 | -------------------------------------------------------------------------------- /tests/assets/es6/es6module.js: -------------------------------------------------------------------------------- 1 | export default 42; 2 | -------------------------------------------------------------------------------- /tests/assets/es6/es6pathimport.js: -------------------------------------------------------------------------------- 1 | import num from './es6/es6module.js'; 2 | window.__es6injected = num; 3 | -------------------------------------------------------------------------------- /tests/assets/file-to-upload-2.txt: -------------------------------------------------------------------------------- 1 | contents of the file 2 | -------------------------------------------------------------------------------- /tests/assets/file-to-upload.txt: -------------------------------------------------------------------------------- 1 | contents of the file 2 | -------------------------------------------------------------------------------- /tests/assets/frames/child-redirect.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/assets/frames/frame.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 |
Hi, I'm frame
16 | -------------------------------------------------------------------------------- /tests/assets/frames/frameset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/assets/frames/nested-frames.html: -------------------------------------------------------------------------------- 1 | 19 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tests/assets/frames/one-frame.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/assets/frames/redirect-my-parent.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /tests/assets/frames/script.js: -------------------------------------------------------------------------------- 1 | console.log('Cheers!'); 2 | -------------------------------------------------------------------------------- /tests/assets/frames/style.css: -------------------------------------------------------------------------------- 1 | div { 2 | color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /tests/assets/frames/two-frames.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/assets/geolocation.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /tests/assets/global-var.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /tests/assets/grid.html: -------------------------------------------------------------------------------- 1 | 28 | 29 | 53 | -------------------------------------------------------------------------------- /tests/assets/har-sha1-main-response.txt: -------------------------------------------------------------------------------- 1 | Hello, world -------------------------------------------------------------------------------- /tests/assets/har.html: -------------------------------------------------------------------------------- 1 | HAR Page 2 | 3 |
hello, world!
4 | -------------------------------------------------------------------------------- /tests/assets/headings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Headings 6 | 7 | 8 | 9 |

Title

10 |

Subtitle

11 |

Subsubtitle

12 |

Subtitle

13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/assets/historyapi.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /tests/assets/injectedfile.js: -------------------------------------------------------------------------------- 1 | window.__injected = 42; 2 | window.injected = 123; 3 | window.__injectedError = new Error('hi'); 4 | -------------------------------------------------------------------------------- /tests/assets/injectedstyle.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: red; 3 | } 4 | -------------------------------------------------------------------------------- /tests/assets/input/animating-button.html: -------------------------------------------------------------------------------- 1 | 8 | 43 | -------------------------------------------------------------------------------- /tests/assets/input/button.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Button test 5 | 6 | 7 | 8 | 9 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/assets/input/checkbox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Selection Test 5 | 6 | 7 | 8 | 9 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /tests/assets/input/fileupload-multi.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | File upload test 5 | 6 | 7 |
8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/assets/input/fileupload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | File upload test 5 | 6 | 7 |
8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/assets/input/folderupload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Folder upload test 5 | 6 | 7 |
8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/assets/input/keyboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Keyboard test 5 | 6 | 7 | 8 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /tests/assets/input/mouse-helper.js: -------------------------------------------------------------------------------- 1 | // This injects a box into the page that moves with the mouse; 2 | // Useful for debugging 3 | (function(){ 4 | const box = document.createElement('div'); 5 | box.classList.add('mouse-helper'); 6 | const styleElement = document.createElement('style'); 7 | styleElement.innerHTML = ` 8 | .mouse-helper { 9 | pointer-events: none; 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | width: 20px; 14 | height: 20px; 15 | background: rgba(0,0,0,.4); 16 | border: 1px solid white; 17 | border-radius: 10px; 18 | margin-left: -10px; 19 | margin-top: -10px; 20 | transition: background .2s, border-radius .2s, border-color .2s; 21 | } 22 | .mouse-helper.button-1 { 23 | transition: none; 24 | background: rgba(0,0,0,0.9); 25 | } 26 | .mouse-helper.button-2 { 27 | transition: none; 28 | border-color: rgba(0,0,255,0.9); 29 | } 30 | .mouse-helper.button-3 { 31 | transition: none; 32 | border-radius: 4px; 33 | } 34 | .mouse-helper.button-4 { 35 | transition: none; 36 | border-color: rgba(255,0,0,0.9); 37 | } 38 | .mouse-helper.button-5 { 39 | transition: none; 40 | border-color: rgba(0,255,0,0.9); 41 | } 42 | `; 43 | document.head.appendChild(styleElement); 44 | document.body.appendChild(box); 45 | document.addEventListener('mousemove', event => { 46 | box.style.left = event.pageX + 'px'; 47 | box.style.top = event.pageY + 'px'; 48 | updateButtons(event.buttons); 49 | }, true); 50 | document.addEventListener('mousedown', event => { 51 | updateButtons(event.buttons); 52 | box.classList.add('button-' + event.which); 53 | }, true); 54 | document.addEventListener('mouseup', event => { 55 | updateButtons(event.buttons); 56 | box.classList.remove('button-' + event.which); 57 | }, true); 58 | function updateButtons(buttons) { 59 | for (let i = 0; i < 5; i++) 60 | box.classList.toggle('button-' + i, buttons & (1 << i)); 61 | } 62 | })(); 63 | -------------------------------------------------------------------------------- /tests/assets/input/rotatedButton.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Rotated button test 5 | 6 | 7 | 8 | 9 | 14 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/assets/input/scrollable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scrollable test 5 | 6 | 7 | 8 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/assets/input/select.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Selection Test 5 | 6 | 7 | 24 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /tests/assets/input/textarea.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Textarea test 5 | 6 | 7 | 8 | 9 |
10 |
Plain div
11 | 12 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/assets/input/touches.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Touch test 5 | 6 | 7 | 8 | 9 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/assets/mobile.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/assets/networkidle.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/assets/networkidle.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | window.ws = new WebSocket('ws://localhost:' + window.location.port + '/ws'); 3 | window.ws.addEventListener('message', message => {}); 4 | 5 | fetch('fetch-request-a.js'); 6 | window.top.fetchSecond = () => { 7 | // Do not return the promise here. 8 | fetch('fetch-request-b.js'); 9 | }; 10 | } 11 | 12 | main(); 13 | -------------------------------------------------------------------------------- /tests/assets/offscreenbuttons.html: -------------------------------------------------------------------------------- 1 | 37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
50 | 56 | -------------------------------------------------------------------------------- /tests/assets/one-style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: pink; 3 | } 4 | -------------------------------------------------------------------------------- /tests/assets/one-style.html: -------------------------------------------------------------------------------- 1 | 2 |
hello, world!
3 | -------------------------------------------------------------------------------- /tests/assets/playground.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Playground 5 | 6 | 7 | 8 | 9 |
First div
10 |
11 | Second div 12 | Inner span 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/assets/popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Popup 5 | 8 | 9 | 10 | I am a popup 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/assets/popup/window-open.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Popup test 5 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/assets/pptr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/assets/pptr.png -------------------------------------------------------------------------------- /tests/assets/react.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 33 | 34 | -------------------------------------------------------------------------------- /tests/assets/sectionselectorengine.js: -------------------------------------------------------------------------------- 1 | ({ 2 | create(root, target) { 3 | }, 4 | query(root, selector) { 5 | return root.querySelector('section'); 6 | }, 7 | queryAll(root, selector) { 8 | return Array.from(root.querySelectorAll('section')); 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /tests/assets/self-request.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /tests/assets/serviceworkers/empty/sw.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /tests/assets/serviceworkers/empty/sw.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/assets/serviceworkers/empty/sw.js -------------------------------------------------------------------------------- /tests/assets/serviceworkers/fetch/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: pink; 3 | } 4 | -------------------------------------------------------------------------------- /tests/assets/serviceworkers/fetch/sw.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /tests/assets/serviceworkers/fetch/sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('fetch', event => { 2 | event.respondWith(fetch(event.request)); 3 | }); 4 | 5 | self.addEventListener('activate', event => { 6 | event.waitUntil(clients.claim()); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/assets/serviceworkers/fetchdummy/sw.html: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /tests/assets/serviceworkers/fetchdummy/sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('fetch', event => { 2 | if (event.request.url.endsWith('.html') || event.request.url.includes('passthrough')) { 3 | event.respondWith(fetch(event.request)); 4 | return; 5 | } 6 | const slash = event.request.url.lastIndexOf('/'); 7 | const name = event.request.url.substring(slash + 1); 8 | const blob = new Blob(["responseFromServiceWorker:" + name], {type : 'text/css'}); 9 | const response = new Response(blob, { "status" : 200 , "statusText" : "OK" }); 10 | event.respondWith(response); 11 | }); 12 | 13 | self.addEventListener('activate', event => { 14 | event.waitUntil(clients.claim()); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/assets/shadow.html: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /tests/assets/simple-extension/content-script.js: -------------------------------------------------------------------------------- 1 | console.log('hey from the content-script'); 2 | self.thisIsTheContentScript = true; 3 | -------------------------------------------------------------------------------- /tests/assets/simple-extension/index.js: -------------------------------------------------------------------------------- 1 | // Mock script for background extension 2 | window.MAGIC = 42; 3 | -------------------------------------------------------------------------------- /tests/assets/simple-extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Simple extension", 3 | "version": "0.1", 4 | "background": { 5 | "scripts": ["index.js"] 6 | }, 7 | "content_scripts": [{ 8 | "matches": [""], 9 | "css": [], 10 | "js": ["content-script.js"] 11 | }], 12 | "permissions": ["background", "activeTab"], 13 | "manifest_version": 2 14 | } 15 | -------------------------------------------------------------------------------- /tests/assets/simple.json: -------------------------------------------------------------------------------- 1 | {"foo": "bar"} 2 | -------------------------------------------------------------------------------- /tests/assets/title.html: -------------------------------------------------------------------------------- 1 | Woof-Woof 2 | -------------------------------------------------------------------------------- /tests/assets/worker/worker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Worker test 5 | 6 | 7 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/assets/worker/worker.js: -------------------------------------------------------------------------------- 1 | console.log('hello from the worker'); 2 | 3 | function workerFunction() { 4 | return 'worker function result'; 5 | } 6 | 7 | self.addEventListener('message', event => { 8 | console.log('got this data: ' + event.data); 9 | }); 10 | 11 | (async function() { 12 | while (true) { 13 | self.postMessage(workerFunction.toString()); 14 | await new Promise(x => setTimeout(x, 100)); 15 | } 16 | })(); 17 | -------------------------------------------------------------------------------- /tests/assets/wrappedlink.html: -------------------------------------------------------------------------------- 1 | 25 |
26 | 123321 27 |
28 | 33 | -------------------------------------------------------------------------------- /tests/async/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/async/__init__.py -------------------------------------------------------------------------------- /tests/async/test_browser.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import re 16 | 17 | import pytest 18 | 19 | from playwright.async_api import Browser, BrowserType, Error 20 | 21 | 22 | async def test_should_create_new_page(browser: Browser) -> None: 23 | page1 = await browser.new_page() 24 | assert len(browser.contexts) == 1 25 | 26 | page2 = await browser.new_page() 27 | assert len(browser.contexts) == 2 28 | 29 | await page1.close() 30 | assert len(browser.contexts) == 1 31 | 32 | await page2.close() 33 | assert len(browser.contexts) == 0 34 | 35 | 36 | async def test_should_throw_upon_second_create_new_page(browser: Browser) -> None: 37 | page = await browser.new_page() 38 | with pytest.raises(Error) as exc: 39 | await page.context.new_page() 40 | await page.close() 41 | assert "Please use browser.new_context()" in exc.value.message 42 | 43 | 44 | async def test_version_should_work(browser: Browser, is_chromium: bool) -> None: 45 | version = browser.version 46 | if is_chromium: 47 | assert re.match(r"^\d+\.\d+\.\d+\.\d+$", version) 48 | else: 49 | assert re.match(r"^\d+\.\d+", version) 50 | 51 | 52 | async def test_should_return_browser_type( 53 | browser: Browser, browser_type: BrowserType 54 | ) -> None: 55 | assert browser.browser_type is browser_type 56 | -------------------------------------------------------------------------------- /tests/async/test_browsercontext_service_worker_policy.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from playwright.async_api import Browser 15 | from tests.server import Server 16 | 17 | 18 | async def test_should_allow_service_workers_by_default( 19 | browser: Browser, server: Server 20 | ) -> None: 21 | context = await browser.new_context() 22 | page = await context.new_page() 23 | await page.goto(server.PREFIX + "/serviceworkers/fetchdummy/sw.html") 24 | await page.evaluate("() => window.activationPromise") 25 | await context.close() 26 | 27 | 28 | async def test_block_blocks_service_worker_registration( 29 | browser: Browser, server: Server 30 | ) -> None: 31 | context = await browser.new_context(service_workers="block") 32 | page = await context.new_page() 33 | async with page.expect_console_message( 34 | lambda m: "Service Worker registration blocked by Playwright" == m.text 35 | ): 36 | await page.goto(server.PREFIX + "/serviceworkers/fetchdummy/sw.html") 37 | await context.close() 38 | -------------------------------------------------------------------------------- /tests/async/test_context_manager.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Dict 16 | 17 | import pytest 18 | 19 | from playwright.async_api import BrowserContext, BrowserType 20 | 21 | 22 | async def test_context_managers( 23 | browser_type: BrowserType, launch_arguments: Dict 24 | ) -> None: 25 | async with await browser_type.launch(**launch_arguments) as browser: 26 | async with await browser.new_context() as context: 27 | async with await context.new_page(): 28 | assert len(context.pages) == 1 29 | assert len(context.pages) == 0 30 | assert len(browser.contexts) == 1 31 | assert len(browser.contexts) == 0 32 | assert not browser.is_connected() 33 | 34 | 35 | async def test_context_managers_not_hang(context: BrowserContext) -> None: 36 | with pytest.raises(Exception, match="Oops!"): 37 | async with await context.new_page(): 38 | raise Exception("Oops!") 39 | -------------------------------------------------------------------------------- /tests/async/test_device_descriptors.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from typing import Dict 15 | 16 | import pytest 17 | 18 | from playwright.async_api import Playwright 19 | 20 | 21 | @pytest.mark.only_browser("chromium") 22 | async def test_should_work(playwright: Playwright, launch_arguments: Dict) -> None: 23 | device_descriptor = playwright.devices["Pixel 2"] 24 | device_type = device_descriptor["default_browser_type"] 25 | browser = await playwright[device_type].launch(**launch_arguments) 26 | context = await browser.new_context( 27 | **device_descriptor, 28 | ) 29 | page = await context.new_page() 30 | assert device_descriptor["default_browser_type"] == "chromium" 31 | assert browser.browser_type.name == "chromium" 32 | 33 | assert "Pixel 2" in device_descriptor["user_agent"] 34 | assert "Pixel 2" in await page.evaluate("navigator.userAgent") 35 | 36 | assert device_descriptor["device_scale_factor"] > 2 37 | assert await page.evaluate("window.devicePixelRatio") > 2 38 | 39 | assert device_descriptor["viewport"]["height"] > 700 40 | assert device_descriptor["viewport"]["height"] < 800 41 | inner_height = await page.evaluate("window.screen.availHeight") 42 | assert inner_height > 700 43 | assert inner_height < 800 44 | 45 | assert device_descriptor["viewport"]["width"] > 400 46 | assert device_descriptor["viewport"]["width"] < 500 47 | inner_width = await page.evaluate("window.screen.availWidth") 48 | assert inner_width > 400 49 | assert inner_width < 500 50 | 51 | assert device_descriptor["has_touch"] 52 | assert device_descriptor["is_mobile"] 53 | 54 | await browser.close() 55 | -------------------------------------------------------------------------------- /tests/async/test_fill.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from playwright.async_api import Page 16 | from tests.server import Server 17 | 18 | 19 | async def test_fill_textarea(page: Page, server: Server) -> None: 20 | await page.goto(f"{server.PREFIX}/input/textarea.html") 21 | await page.fill("textarea", "some value") 22 | assert await page.evaluate("result") == "some value" 23 | 24 | 25 | async def test_is_enabled_for_non_editable_button(page: Page) -> None: 26 | await page.set_content( 27 | """ 28 | 29 | """ 30 | ) 31 | button = page.locator("button") 32 | assert await button.is_enabled() is True 33 | 34 | 35 | async def test_fill_input(page: Page, server: Server) -> None: 36 | await page.goto(f"{server.PREFIX}/input/textarea.html") 37 | await page.fill("input", "some value") 38 | assert await page.evaluate("result") == "some value" 39 | -------------------------------------------------------------------------------- /tests/async/test_ignore_https_errors.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import pytest 16 | 17 | from playwright.async_api import Browser, Error 18 | from tests.server import Server 19 | 20 | 21 | async def test_ignore_https_error_should_work( 22 | browser: Browser, https_server: Server 23 | ) -> None: 24 | context = await browser.new_context(ignore_https_errors=True) 25 | page = await context.new_page() 26 | response = await page.goto(https_server.EMPTY_PAGE) 27 | assert response 28 | assert response.ok 29 | await context.close() 30 | 31 | 32 | async def test_ignore_https_error_should_work_negative_case( 33 | browser: Browser, https_server: Server 34 | ) -> None: 35 | context = await browser.new_context() 36 | page = await context.new_page() 37 | with pytest.raises(Error): 38 | await page.goto(https_server.EMPTY_PAGE) 39 | await context.close() 40 | -------------------------------------------------------------------------------- /tests/async/test_issues.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from asyncio import FIRST_COMPLETED, CancelledError, create_task, wait 16 | from typing import Dict 17 | 18 | import pytest 19 | 20 | from playwright.async_api import Browser, BrowserType, Page, Playwright 21 | 22 | 23 | @pytest.mark.only_browser("chromium") 24 | async def test_issue_189(browser_type: BrowserType, launch_arguments: Dict) -> None: 25 | browser = await browser_type.launch( 26 | **launch_arguments, ignore_default_args=["--mute-audio"] 27 | ) 28 | page = await browser.new_page() 29 | assert await page.evaluate("1 + 1") == 2 30 | await browser.close() 31 | 32 | 33 | @pytest.mark.only_browser("chromium") 34 | async def test_issue_195(playwright: Playwright, browser: Browser) -> None: 35 | iphone_11 = playwright.devices["iPhone 11"] 36 | context = await browser.new_context(**iphone_11) 37 | await context.close() 38 | 39 | 40 | async def test_connection_task_cancel(page: Page) -> None: 41 | await page.set_content("") 42 | done, pending = await wait( 43 | { 44 | create_task(page.wait_for_selector("input")), 45 | create_task(page.wait_for_selector("#will-never-resolve")), 46 | }, 47 | return_when=FIRST_COMPLETED, 48 | ) 49 | assert len(done) == 1 50 | assert len(pending) == 1 51 | for task in pending: 52 | task.cancel() 53 | with pytest.raises(CancelledError): 54 | await task 55 | assert list(pending)[0].cancelled() 56 | -------------------------------------------------------------------------------- /tests/async/test_listeners.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from playwright.async_api import Page, Response 16 | from tests.server import Server 17 | 18 | 19 | async def test_listeners(page: Page, server: Server) -> None: 20 | log = [] 21 | 22 | def print_response(response: Response) -> None: 23 | log.append(response) 24 | 25 | page.on("response", print_response) 26 | await page.goto(f"{server.PREFIX}/input/textarea.html") 27 | assert len(log) > 0 28 | page.remove_listener("response", print_response) 29 | 30 | log = [] 31 | await page.goto(f"{server.PREFIX}/input/textarea.html") 32 | assert len(log) == 0 33 | -------------------------------------------------------------------------------- /tests/async/test_page_network_request.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import asyncio 16 | 17 | import pytest 18 | 19 | from playwright.async_api import Error, Page, Request 20 | from tests.server import Server 21 | 22 | 23 | async def test_should_not_allow_to_access_frame_on_popup_main_request( 24 | page: Page, server: Server 25 | ) -> None: 26 | await page.set_content(f'click me') 27 | request_promise = asyncio.ensure_future(page.context.wait_for_event("request")) 28 | popup_promise = asyncio.ensure_future(page.context.wait_for_event("page")) 29 | clicked = asyncio.ensure_future(page.get_by_text("click me").click()) 30 | request: Request = await request_promise 31 | 32 | assert request.is_navigation_request() 33 | 34 | with pytest.raises(Error) as exc_info: 35 | request.frame 36 | assert ( 37 | "Frame for this navigation request is not available" in exc_info.value.message 38 | ) 39 | 40 | response = await request.response() 41 | assert response 42 | await response.finished() 43 | await popup_promise 44 | await clicked 45 | 46 | 47 | async def test_should_parse_the_data_if_content_type_is_application_x_www_form_urlencoded_charset_UTF_8( 48 | page: Page, server: Server 49 | ) -> None: 50 | await page.goto(server.EMPTY_PAGE) 51 | async with page.expect_event("request") as request_info: 52 | await page.evaluate( 53 | """() => fetch('./post', { 54 | method: 'POST', 55 | headers: { 56 | 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', 57 | }, 58 | body: 'foo=bar&baz=123' 59 | })""" 60 | ) 61 | request = await request_info.value 62 | assert request 63 | assert request.post_data_json == {"foo": "bar", "baz": "123"} 64 | -------------------------------------------------------------------------------- /tests/async/test_page_network_response.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import asyncio 16 | 17 | import pytest 18 | 19 | from playwright.async_api import Error, Page 20 | from tests.server import Server, TestServerRequest 21 | 22 | 23 | async def test_should_reject_response_finished_if_page_closes( 24 | page: Page, server: Server 25 | ) -> None: 26 | await page.goto(server.EMPTY_PAGE) 27 | 28 | def handle_get(request: TestServerRequest) -> None: 29 | # In Firefox, |fetch| will be hanging until it receives |Content-Type| header 30 | # from server. 31 | request.setHeader("Content-Type", "text/plain; charset=utf-8") 32 | request.write(b"hello ") 33 | 34 | server.set_route("/get", handle_get) 35 | # send request and wait for server response 36 | [page_response, _] = await asyncio.gather( 37 | page.wait_for_event("response"), 38 | page.evaluate("() => fetch('./get', { method: 'GET' })"), 39 | ) 40 | 41 | finish_coroutine = page_response.finished() 42 | await page.close() 43 | with pytest.raises(Error) as exc_info: 44 | await finish_coroutine 45 | error = exc_info.value 46 | assert "closed" in error.message 47 | 48 | 49 | async def test_should_reject_response_finished_if_context_closes( 50 | page: Page, server: Server 51 | ) -> None: 52 | await page.goto(server.EMPTY_PAGE) 53 | 54 | def handle_get(request: TestServerRequest) -> None: 55 | # In Firefox, |fetch| will be hanging until it receives |Content-Type| header 56 | # from server. 57 | request.setHeader("Content-Type", "text/plain; charset=utf-8") 58 | request.write(b"hello ") 59 | 60 | server.set_route("/get", handle_get) 61 | # send request and wait for server response 62 | [page_response, _] = await asyncio.gather( 63 | page.wait_for_event("response"), 64 | page.evaluate("() => fetch('./get', { method: 'GET' })"), 65 | ) 66 | 67 | finish_coroutine = page_response.finished() 68 | await page.context.close() 69 | with pytest.raises(Error) as exc_info: 70 | await finish_coroutine 71 | error = exc_info.value 72 | assert "closed" in error.message 73 | -------------------------------------------------------------------------------- /tests/async/test_page_request_gc.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from playwright.async_api import Page 16 | from tests.server import Server 17 | 18 | 19 | async def test_should_work(page: Page, server: Server) -> None: 20 | await page.evaluate( 21 | """() => { 22 | globalThis.objectToDestroy = { hello: 'world' }; 23 | globalThis.weakRef = new WeakRef(globalThis.objectToDestroy); 24 | }""" 25 | ) 26 | await page.request_gc() 27 | assert await page.evaluate("() => globalThis.weakRef.deref()") == {"hello": "world"} 28 | 29 | await page.request_gc() 30 | assert await page.evaluate("() => globalThis.weakRef.deref()") == {"hello": "world"} 31 | 32 | await page.evaluate("() => globalThis.objectToDestroy = null") 33 | await page.request_gc() 34 | assert await page.evaluate("() => globalThis.weakRef.deref()") is None 35 | -------------------------------------------------------------------------------- /tests/async/test_pdf.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | from pathlib import Path 17 | 18 | import pytest 19 | 20 | from playwright.async_api import Page 21 | from tests.server import Server 22 | 23 | pytestmark = pytest.mark.only_browser("chromium") 24 | 25 | 26 | async def test_should_be_able_to_save_pdf_file(page: Page, tmpdir: Path) -> None: 27 | output_file = tmpdir / "foo.png" 28 | await page.pdf(path=str(output_file)) 29 | assert os.path.getsize(output_file) > 0 30 | 31 | 32 | async def test_should_be_able_capture_pdf_without_path(page: Page) -> None: 33 | buffer = await page.pdf() 34 | assert buffer 35 | 36 | 37 | async def test_should_be_able_to_generate_outline( 38 | page: Page, server: Server, tmpdir: Path 39 | ) -> None: 40 | await page.goto(server.PREFIX + "/headings.html") 41 | output_file_no_outline = tmpdir / "outputNoOutline.pdf" 42 | output_file_outline = tmpdir / "outputOutline.pdf" 43 | await page.pdf(path=output_file_no_outline) 44 | await page.pdf(path=output_file_outline, tagged=True, outline=True) 45 | assert os.path.getsize(output_file_outline) > os.path.getsize( 46 | output_file_no_outline 47 | ) 48 | -------------------------------------------------------------------------------- /tests/async/test_screenshot.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Callable 16 | 17 | from playwright.async_api import Page 18 | from tests.server import Server 19 | from tests.utils import must 20 | 21 | 22 | async def test_should_screenshot_with_mask( 23 | page: Page, server: Server, assert_to_be_golden: Callable[[bytes, str], None] 24 | ) -> None: 25 | await page.set_viewport_size( 26 | { 27 | "width": 500, 28 | "height": 500, 29 | } 30 | ) 31 | await page.goto(server.PREFIX + "/grid.html") 32 | assert_to_be_golden( 33 | await page.screenshot(mask=[page.locator("div").nth(5)]), 34 | "mask-should-work-with-page.png", 35 | ) 36 | assert_to_be_golden( 37 | await page.locator("body").screenshot(mask=[page.locator("div").nth(5)]), 38 | "mask-should-work-with-locator.png", 39 | ) 40 | assert_to_be_golden( 41 | await must(await page.query_selector("body")).screenshot( 42 | mask=[page.locator("div").nth(5)] 43 | ), 44 | "mask-should-work-with-element-handle.png", 45 | ) 46 | -------------------------------------------------------------------------------- /tests/async/test_selector_generator.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import pytest 16 | 17 | from playwright.async_api import Error, Page, Playwright 18 | 19 | 20 | async def test_should_use_data_test_id_in_strict_errors( 21 | page: Page, playwright: Playwright 22 | ) -> None: 23 | playwright.selectors.set_test_id_attribute("data-custom-id") 24 | try: 25 | await page.set_content( 26 | """ 27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | """ 41 | ) 42 | with pytest.raises(Error) as exc_info: 43 | await page.locator(".foo").hover() 44 | assert "strict mode violation" in exc_info.value.message 45 | assert '
None: 19 | await page.set_content( 20 | """ 21 |
hello
world
22 | hello2world2 23 | """ 24 | ) 25 | assert ( 26 | await page.eval_on_selector_all( 27 | 'div >> internal:and="span"', "els => els.map(e => e.textContent)" 28 | ) 29 | ) == [] 30 | assert ( 31 | await page.eval_on_selector_all( 32 | 'div >> internal:and=".foo"', "els => els.map(e => e.textContent)" 33 | ) 34 | ) == ["hello"] 35 | assert ( 36 | await page.eval_on_selector_all( 37 | 'div >> internal:and=".bar"', "els => els.map(e => e.textContent)" 38 | ) 39 | ) == ["world"] 40 | assert ( 41 | await page.eval_on_selector_all( 42 | 'span >> internal:and="span"', "els => els.map(e => e.textContent)" 43 | ) 44 | ) == ["hello2", "world2"] 45 | assert ( 46 | await page.eval_on_selector_all( 47 | '.foo >> internal:and="div"', "els => els.map(e => e.textContent)" 48 | ) 49 | ) == ["hello"] 50 | assert ( 51 | await page.eval_on_selector_all( 52 | '.bar >> internal:and="span"', "els => els.map(e => e.textContent)" 53 | ) 54 | ) == ["world2"] 55 | -------------------------------------------------------------------------------- /tests/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/common/__init__.py -------------------------------------------------------------------------------- /tests/common/test_collect_handles.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/common/test_collect_handles.py -------------------------------------------------------------------------------- /tests/common/test_events.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from typing import Dict 15 | 16 | import pytest 17 | 18 | from playwright.sync_api import sync_playwright 19 | from tests.server import Server 20 | 21 | 22 | def test_events(browser_name: str, launch_arguments: Dict, server: Server) -> None: 23 | with pytest.raises(Exception, match="fail"): 24 | 25 | def fail() -> None: 26 | raise Exception("fail") 27 | 28 | with sync_playwright() as p: 29 | with p[browser_name].launch(**launch_arguments) as browser: 30 | with browser.new_page() as page: 31 | page.on("response", lambda _: fail()) 32 | page.goto(server.PREFIX + "/grid.html") 33 | -------------------------------------------------------------------------------- /tests/common/test_threads.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import threading 16 | from typing import Dict 17 | 18 | from playwright.sync_api import sync_playwright 19 | 20 | 21 | def test_running_in_thread(browser_name: str, launch_arguments: Dict) -> None: 22 | result = [] 23 | 24 | class TestThread(threading.Thread): 25 | def run(self) -> None: 26 | with sync_playwright() as playwright: 27 | browser = playwright[browser_name].launch(**launch_arguments) 28 | # This should not throw ^^. 29 | browser.new_page() 30 | browser.close() 31 | result.append("Success") 32 | 33 | test_thread = TestThread() 34 | test_thread.start() 35 | test_thread.join() 36 | assert "Success" in result 37 | -------------------------------------------------------------------------------- /tests/golden-chromium/grid-cell-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-chromium/grid-cell-0.png -------------------------------------------------------------------------------- /tests/golden-chromium/mask-should-work-with-element-handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-chromium/mask-should-work-with-element-handle.png -------------------------------------------------------------------------------- /tests/golden-chromium/mask-should-work-with-locator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-chromium/mask-should-work-with-locator.png -------------------------------------------------------------------------------- /tests/golden-chromium/mask-should-work-with-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-chromium/mask-should-work-with-page.png -------------------------------------------------------------------------------- /tests/golden-chromium/mock-binary-response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-chromium/mock-binary-response.png -------------------------------------------------------------------------------- /tests/golden-chromium/mock-svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-chromium/mock-svg.png -------------------------------------------------------------------------------- /tests/golden-chromium/screenshot-element-bounding-box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-chromium/screenshot-element-bounding-box.png -------------------------------------------------------------------------------- /tests/golden-chromium/screenshot-sanity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-chromium/screenshot-sanity.png -------------------------------------------------------------------------------- /tests/golden-firefox/grid-cell-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-firefox/grid-cell-0.png -------------------------------------------------------------------------------- /tests/golden-firefox/mask-should-work-with-element-handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-firefox/mask-should-work-with-element-handle.png -------------------------------------------------------------------------------- /tests/golden-firefox/mask-should-work-with-locator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-firefox/mask-should-work-with-locator.png -------------------------------------------------------------------------------- /tests/golden-firefox/mask-should-work-with-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-firefox/mask-should-work-with-page.png -------------------------------------------------------------------------------- /tests/golden-firefox/mock-binary-response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-firefox/mock-binary-response.png -------------------------------------------------------------------------------- /tests/golden-firefox/mock-svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-firefox/mock-svg.png -------------------------------------------------------------------------------- /tests/golden-firefox/screenshot-element-bounding-box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-firefox/screenshot-element-bounding-box.png -------------------------------------------------------------------------------- /tests/golden-firefox/screenshot-sanity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-firefox/screenshot-sanity.png -------------------------------------------------------------------------------- /tests/golden-webkit/grid-cell-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-webkit/grid-cell-0.png -------------------------------------------------------------------------------- /tests/golden-webkit/mask-should-work-with-element-handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-webkit/mask-should-work-with-element-handle.png -------------------------------------------------------------------------------- /tests/golden-webkit/mask-should-work-with-locator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-webkit/mask-should-work-with-locator.png -------------------------------------------------------------------------------- /tests/golden-webkit/mask-should-work-with-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-webkit/mask-should-work-with-page.png -------------------------------------------------------------------------------- /tests/golden-webkit/mock-binary-response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-webkit/mock-binary-response.png -------------------------------------------------------------------------------- /tests/golden-webkit/mock-svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-webkit/mock-svg.png -------------------------------------------------------------------------------- /tests/golden-webkit/screenshot-element-bounding-box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-webkit/screenshot-element-bounding-box.png -------------------------------------------------------------------------------- /tests/golden-webkit/screenshot-sanity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/golden-webkit/screenshot-sanity.png -------------------------------------------------------------------------------- /tests/sync/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/playwright-python/e87e340fae92cb52f98dacdd80bb55c04e9bc4e3/tests/sync/__init__.py -------------------------------------------------------------------------------- /tests/sync/test_browser.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from playwright.sync_api import Browser, BrowserType 16 | 17 | 18 | def test_should_return_browser_type( 19 | browser: Browser, browser_type: BrowserType 20 | ) -> None: 21 | assert browser.browser_type is browser_type 22 | -------------------------------------------------------------------------------- /tests/sync/test_browsercontext_service_worker_policy.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | from playwright.sync_api import Browser 15 | from tests.server import Server 16 | 17 | 18 | def test_should_allow_service_workers_by_default( 19 | browser: Browser, server: Server 20 | ) -> None: 21 | context = browser.new_context() 22 | page = context.new_page() 23 | page.goto(server.PREFIX + "/serviceworkers/fetchdummy/sw.html") 24 | page.evaluate("() => window.activationPromise") 25 | context.close() 26 | 27 | 28 | def test_block_blocks_service_worker_registration( 29 | browser: Browser, server: Server 30 | ) -> None: 31 | context = browser.new_context(service_workers="block") 32 | page = context.new_page() 33 | with page.expect_console_message( 34 | lambda m: "Service Worker registration blocked by Playwright" == m.text 35 | ): 36 | page.goto(server.PREFIX + "/serviceworkers/fetchdummy/sw.html") 37 | context.close() 38 | -------------------------------------------------------------------------------- /tests/sync/test_browsertype_connect_cdp.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Dict 16 | 17 | import pytest 18 | 19 | from playwright.sync_api import BrowserType 20 | from tests.server import find_free_port 21 | 22 | pytestmark = pytest.mark.only_browser("chromium") 23 | 24 | 25 | def test_connect_to_an_existing_cdp_session( 26 | launch_arguments: Dict, browser_type: BrowserType 27 | ) -> None: 28 | port = find_free_port() 29 | browser_server = browser_type.launch( 30 | **launch_arguments, args=[f"--remote-debugging-port={port}"] 31 | ) 32 | cdp_browser = browser_type.connect_over_cdp(f"http://127.0.0.1:{port}") 33 | assert len(cdp_browser.contexts) == 1 34 | cdp_browser.close() 35 | browser_server.close() 36 | -------------------------------------------------------------------------------- /tests/sync/test_context_manager.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Dict 16 | 17 | import pytest 18 | 19 | from playwright.sync_api import BrowserContext, BrowserType 20 | 21 | 22 | def test_context_managers(browser_type: BrowserType, launch_arguments: Dict) -> None: 23 | with browser_type.launch(**launch_arguments) as browser: 24 | with browser.new_context() as context: 25 | with context.new_page(): 26 | assert len(context.pages) == 1 27 | assert len(context.pages) == 0 28 | assert len(browser.contexts) == 1 29 | assert len(browser.contexts) == 0 30 | assert not browser.is_connected() 31 | 32 | 33 | def test_context_managers_not_hang(context: BrowserContext) -> None: 34 | with pytest.raises(Exception, match="Oops!"): 35 | with context.new_page(): 36 | raise Exception("Oops!") 37 | -------------------------------------------------------------------------------- /tests/sync/test_fill.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from playwright.sync_api import Page 16 | from tests.server import Server 17 | 18 | 19 | def test_fill_textarea(page: Page, server: Server) -> None: 20 | page.goto(f"{server.PREFIX}/input/textarea.html") 21 | page.fill("textarea", "some value") 22 | assert page.evaluate("result") == "some value" 23 | 24 | 25 | def test_fill_input(page: Page, server: Server) -> None: 26 | page.goto(f"{server.PREFIX}/input/textarea.html") 27 | page.fill("input", "some value") 28 | assert page.evaluate("result") == "some value" 29 | -------------------------------------------------------------------------------- /tests/sync/test_input.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | from pathlib import Path 17 | from typing import Any 18 | 19 | from playwright.sync_api import Page 20 | 21 | 22 | def test_expect_file_chooser(page: Page) -> None: 23 | page.set_content("") 24 | with page.expect_file_chooser() as fc_info: 25 | page.click('input[type="file"]') 26 | fc = fc_info.value 27 | fc.set_files( 28 | {"name": "test.txt", "mimeType": "text/plain", "buffer": b"Hello World"} 29 | ) 30 | 31 | 32 | def test_set_input_files_should_preserve_last_modified_timestamp( 33 | page: Page, 34 | assetdir: Path, 35 | ) -> None: 36 | page.set_content("") 37 | input = page.locator("input") 38 | files: Any = ["file-to-upload.txt", "file-to-upload-2.txt"] 39 | input.set_input_files([assetdir / file for file in files]) 40 | assert input.evaluate("input => [...input.files].map(f => f.name)") == files 41 | timestamps = input.evaluate("input => [...input.files].map(f => f.lastModified)") 42 | expected_timestamps = [os.path.getmtime(assetdir / file) * 1000 for file in files] 43 | 44 | # On Linux browser sometimes reduces the timestamp by 1ms: 1696272058110.0715 -> 1696272058109 or even 45 | # rounds it to seconds in WebKit: 1696272058110 -> 1696272058000. 46 | for i in range(len(timestamps)): 47 | assert abs(timestamps[i] - expected_timestamps[i]) < 1000 48 | -------------------------------------------------------------------------------- /tests/sync/test_listeners.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from playwright.sync_api import Page, Response 17 | from tests.server import Server 18 | 19 | 20 | def test_listeners(page: Page, server: Server) -> None: 21 | log = [] 22 | 23 | def print_response(response: Response) -> None: 24 | log.append(response) 25 | 26 | page.on("response", print_response) 27 | page.goto(f"{server.PREFIX}/input/textarea.html") 28 | assert len(log) > 0 29 | page.remove_listener("response", print_response) 30 | 31 | log = [] 32 | page.goto(f"{server.PREFIX}/input/textarea.html") 33 | assert len(log) == 0 34 | -------------------------------------------------------------------------------- /tests/sync/test_page_network_response.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import pytest 16 | from twisted.web import http 17 | 18 | from playwright.sync_api import Error, Page 19 | from tests.server import Server 20 | 21 | 22 | def test_should_reject_response_finished_if_page_closes( 23 | page: Page, server: Server 24 | ) -> None: 25 | page.goto(server.EMPTY_PAGE) 26 | 27 | def handle_get(request: http.Request) -> None: 28 | # In Firefox, |fetch| will be hanging until it receives |Content-Type| header 29 | # from server. 30 | request.setHeader("Content-Type", "text/plain; charset=utf-8") 31 | request.write(b"hello ") 32 | 33 | server.set_route("/get", handle_get) 34 | # send request and wait for server response 35 | with page.expect_response("**/*") as response_info: 36 | page.evaluate("() => fetch('./get', { method: 'GET' })") 37 | page_response = response_info.value 38 | page.close() 39 | with pytest.raises(Error) as exc_info: 40 | page_response.finished() 41 | error = exc_info.value 42 | assert "closed" in error.message 43 | 44 | 45 | def test_should_reject_response_finished_if_context_closes( 46 | page: Page, server: Server 47 | ) -> None: 48 | page.goto(server.EMPTY_PAGE) 49 | 50 | def handle_get(request: http.Request) -> None: 51 | # In Firefox, |fetch| will be hanging until it receives |Content-Type| header 52 | # from server. 53 | request.setHeader("Content-Type", "text/plain; charset=utf-8") 54 | request.write(b"hello ") 55 | 56 | server.set_route("/get", handle_get) 57 | # send request and wait for server response 58 | with page.expect_response("**/*") as response_info: 59 | page.evaluate("() => fetch('./get', { method: 'GET' })") 60 | page_response = response_info.value 61 | 62 | page.context.close() 63 | with pytest.raises(Error) as exc_info: 64 | page_response.finished() 65 | error = exc_info.value 66 | assert "closed" in error.message 67 | -------------------------------------------------------------------------------- /tests/sync/test_page_request_gc.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from playwright.sync_api import Page 16 | from tests.server import Server 17 | 18 | 19 | def test_should_work(page: Page, server: Server) -> None: 20 | page.evaluate( 21 | """() => { 22 | globalThis.objectToDestroy = { hello: 'world' }; 23 | globalThis.weakRef = new WeakRef(globalThis.objectToDestroy); 24 | }""" 25 | ) 26 | page.request_gc() 27 | assert page.evaluate("() => globalThis.weakRef.deref()") == {"hello": "world"} 28 | 29 | page.request_gc() 30 | assert page.evaluate("() => globalThis.weakRef.deref()") == {"hello": "world"} 31 | 32 | page.evaluate("() => globalThis.objectToDestroy = null") 33 | page.request_gc() 34 | assert page.evaluate("() => globalThis.weakRef.deref()") is None 35 | -------------------------------------------------------------------------------- /tests/sync/test_page_request_intercept.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import pytest 16 | 17 | from playwright.sync_api import Error, Page, Route 18 | from tests.server import Server, TestServerRequest 19 | 20 | 21 | def test_should_support_timeout_option_in_route_fetch( 22 | server: Server, page: Page 23 | ) -> None: 24 | def _handle(request: TestServerRequest) -> None: 25 | request.responseHeaders.addRawHeader("Content-Length", "4096") 26 | request.responseHeaders.addRawHeader("Content-Type", "text/html") 27 | request.write(b"") 28 | 29 | server.set_route( 30 | "/slow", 31 | _handle, 32 | ) 33 | 34 | def handle(route: Route) -> None: 35 | with pytest.raises(Error) as error: 36 | route.fetch(timeout=1000) 37 | assert "Request timed out after 1000ms" in error.value.message 38 | 39 | page.route("**/*", lambda route: handle(route)) 40 | with pytest.raises(Error) as error: 41 | page.goto(server.PREFIX + "/slow", timeout=2000) 42 | assert "Timeout 2000ms exceeded" in error.value.message 43 | 44 | 45 | def test_should_intercept_with_url_override(server: Server, page: Page) -> None: 46 | def handle(route: Route) -> None: 47 | response = route.fetch(url=server.PREFIX + "/one-style.html") 48 | route.fulfill(response=response) 49 | 50 | page.route("**/*.html", lambda route: handle(route)) 51 | response = page.goto(server.PREFIX + "/empty.html") 52 | assert response 53 | assert response.status == 200 54 | assert "one-style.css" in response.body().decode("utf-8") 55 | -------------------------------------------------------------------------------- /tests/sync/test_pdf.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | from pathlib import Path 17 | 18 | import pytest 19 | 20 | from playwright.sync_api import Page 21 | 22 | 23 | @pytest.mark.only_browser("chromium") 24 | def test_should_be_able_to_save_pdf_file(page: Page, tmpdir: Path) -> None: 25 | output_file = tmpdir / "foo.png" 26 | page.pdf(path=str(output_file)) 27 | assert os.path.getsize(output_file) > 0 28 | 29 | 30 | @pytest.mark.only_browser("chromium") 31 | def test_should_be_able_capture_pdf_without_path(page: Page) -> None: 32 | buffer = page.pdf() 33 | assert buffer 34 | -------------------------------------------------------------------------------- /tests/sync/test_request_fulfill.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from playwright.sync_api import Page, Route 16 | from tests.server import Server 17 | 18 | 19 | def test_should_fetch_original_request_and_fulfill(page: Page, server: Server) -> None: 20 | def handle(route: Route) -> None: 21 | response = page.request.fetch(route.request) 22 | route.fulfill(response=response) 23 | 24 | page.route("**/*", handle) 25 | response = page.goto(server.PREFIX + "/title.html") 26 | assert response 27 | assert response.status == 200 28 | assert page.title() == "Woof-Woof" 29 | 30 | 31 | def test_should_fulfill_json(page: Page, server: Server) -> None: 32 | def handle(route: Route) -> None: 33 | route.fulfill(status=201, headers={"foo": "bar"}, json={"bar": "baz"}) 34 | 35 | page.route("**/*", handle) 36 | 37 | response = page.goto(server.EMPTY_PAGE) 38 | assert response 39 | assert response.status == 201 40 | assert response.headers["content-type"] == "application/json" 41 | assert response.json() == {"bar": "baz"} 42 | 43 | 44 | def test_should_fulfill_json_overriding_existing_response( 45 | page: Page, server: Server 46 | ) -> None: 47 | server.set_route( 48 | "/tags", 49 | lambda request: ( 50 | request.setHeader("foo", "bar"), 51 | request.write('{"tags": ["a", "b"]}'.encode()), 52 | request.finish(), 53 | ), 54 | ) 55 | 56 | original = {} 57 | 58 | def handle(route: Route) -> None: 59 | response = route.fetch() 60 | json = response.json() 61 | original["tags"] = json["tags"] 62 | json["tags"] = ["c"] 63 | route.fulfill(response=response, json=json) 64 | 65 | page.route("**/*", handle) 66 | 67 | response = page.goto(server.PREFIX + "/tags") 68 | assert response 69 | assert response.status == 200 70 | assert response.headers["content-type"] == "application/json" 71 | assert response.headers["foo"] == "bar" 72 | assert original["tags"] == ["a", "b"] 73 | assert response.json() == {"tags": ["c"]} 74 | -------------------------------------------------------------------------------- /tests/sync/test_selectors_misc.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from playwright.sync_api import Page 16 | 17 | 18 | def test_should_work_with_internal_and(page: Page) -> None: 19 | page.set_content( 20 | """ 21 |
hello
world
22 | hello2world2 23 | """ 24 | ) 25 | assert ( 26 | page.eval_on_selector_all( 27 | 'div >> internal:and="span"', "els => els.map(e => e.textContent)" 28 | ) 29 | ) == [] 30 | assert ( 31 | page.eval_on_selector_all( 32 | 'div >> internal:and=".foo"', "els => els.map(e => e.textContent)" 33 | ) 34 | ) == ["hello"] 35 | assert ( 36 | page.eval_on_selector_all( 37 | 'div >> internal:and=".bar"', "els => els.map(e => e.textContent)" 38 | ) 39 | ) == ["world"] 40 | assert ( 41 | page.eval_on_selector_all( 42 | 'span >> internal:and="span"', "els => els.map(e => e.textContent)" 43 | ) 44 | ) == ["hello2", "world2"] 45 | assert ( 46 | page.eval_on_selector_all( 47 | '.foo >> internal:and="div"', "els => els.map(e => e.textContent)" 48 | ) 49 | ) == ["hello"] 50 | assert ( 51 | page.eval_on_selector_all( 52 | '.bar >> internal:and="span"', "els => els.map(e => e.textContent)" 53 | ) 54 | ) == ["world2"] 55 | -------------------------------------------------------------------------------- /tests/sync/test_unroute_behavior.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from playwright.sync_api import BrowserContext, Page 16 | from tests.server import Server 17 | from tests.utils import must 18 | 19 | 20 | def test_context_unroute_all_removes_all_handlers( 21 | page: Page, context: BrowserContext, server: Server 22 | ) -> None: 23 | context.route( 24 | "**/*", 25 | lambda route: route.abort(), 26 | ) 27 | context.route( 28 | "**/empty.html", 29 | lambda route: route.abort(), 30 | ) 31 | context.unroute_all() 32 | page.goto(server.EMPTY_PAGE) 33 | 34 | 35 | def test_page_unroute_all_removes_all_routes(page: Page, server: Server) -> None: 36 | page.route( 37 | "**/*", 38 | lambda route: route.abort(), 39 | ) 40 | page.route( 41 | "**/empty.html", 42 | lambda route: route.abort(), 43 | ) 44 | page.unroute_all() 45 | response = must(page.goto(server.EMPTY_PAGE)) 46 | assert response.ok 47 | -------------------------------------------------------------------------------- /tests/test_installation.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import shutil 17 | import subprocess 18 | import sys 19 | from pathlib import Path 20 | from venv import EnvBuilder 21 | 22 | 23 | def test_install(tmp_path: Path, browser_name: str) -> None: 24 | env_dir = tmp_path / "env" 25 | env = EnvBuilder(with_pip=True) 26 | env.create(env_dir=env_dir) 27 | context = env.ensure_directories(env_dir) 28 | root = Path(__file__).parent.parent.resolve() 29 | if sys.platform == "win32": 30 | wheelpath = list((root / "dist").glob("playwright*win_amd64*.whl"))[0] 31 | elif sys.platform == "linux": 32 | wheelpath = list((root / "dist").glob("playwright*manylinux1*.whl"))[0] 33 | elif sys.platform == "darwin": 34 | wheelpath = list((root / "dist").glob("playwright*macosx_*.whl"))[0] 35 | subprocess.check_output( 36 | [ 37 | context.env_exe, 38 | "-m", 39 | "pip", 40 | "install", 41 | str(wheelpath), 42 | ] 43 | ) 44 | environ = os.environ.copy() 45 | environ["PLAYWRIGHT_BROWSERS_PATH"] = str(tmp_path) 46 | subprocess.check_output( 47 | [context.env_exe, "-m", "playwright", "install", browser_name], env=environ 48 | ) 49 | shutil.copyfile(root / "tests" / "assets" / "client.py", tmp_path / "main.py") 50 | subprocess.check_output( 51 | [context.env_exe, str(tmp_path / "main.py"), browser_name], env=environ 52 | ) 53 | assert (tmp_path / f"{browser_name}.png").exists() 54 | -------------------------------------------------------------------------------- /tests/test_reference_count_async.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import gc 16 | from collections import defaultdict 17 | from typing import Any 18 | 19 | import objgraph 20 | import pytest 21 | 22 | from playwright.async_api import async_playwright 23 | from tests.server import Server 24 | 25 | 26 | @pytest.mark.asyncio 27 | async def test_memory_objects(server: Server, browser_name: str) -> None: 28 | async with async_playwright() as p: 29 | browser = await p[browser_name].launch() 30 | page = await browser.new_page() 31 | await page.goto(server.EMPTY_PAGE) 32 | 33 | page.on("dialog", lambda dialog: dialog.dismiss()) 34 | for _ in range(100): 35 | await page.evaluate("""async () => alert()""") 36 | 37 | await page.route("**/*", lambda route, _: route.fulfill(body="OK")) 38 | 39 | def handle_network_response_received(event: Any) -> None: 40 | event["__pw__is_last_network_response_received_event"] = True 41 | 42 | if browser_name == "chromium": 43 | # https://github.com/microsoft/playwright-python/issues/1602 44 | client = await page.context.new_cdp_session(page) 45 | await client.send("Network.enable") 46 | 47 | client.on( 48 | "Network.responseReceived", 49 | handle_network_response_received, 50 | ) 51 | 52 | for _ in range(100): 53 | response = await page.evaluate("""async () => (await fetch("/")).text()""") 54 | assert response == "OK" 55 | 56 | await browser.close() 57 | 58 | gc.collect() 59 | 60 | pw_objects: defaultdict = defaultdict(int) 61 | for o in objgraph.by_type("dict"): 62 | assert isinstance(o, dict) 63 | name = o.get("_type") 64 | # https://github.com/microsoft/playwright-python/issues/1602 65 | if o.get("__pw__is_last_network_response_received_event", False): 66 | assert False 67 | if not name: 68 | continue 69 | pw_objects[name] += 1 70 | 71 | assert "Dialog" not in pw_objects 72 | assert "Request" not in pw_objects 73 | assert "Route" not in pw_objects 74 | -------------------------------------------------------------------------------- /tests/testserver/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEsjCCApoCCQCIPLvQDgoZojANBgkqhkiG9w0BAQsFADAaMRgwFgYDVQQDDA9w 3 | dXBwZXRlZXItdGVzdHMwIBcNMTkwMjEzMTkwNzQzWhgPMzAxODA2MTYxOTA3NDNa 4 | MBoxGDAWBgNVBAMMD3B1cHBldGVlci10ZXN0czCCAiIwDQYJKoZIhvcNAQEBBQAD 5 | ggIPADCCAgoCggIBAJue1yqA4qn0SJR3rgTd6sCYVHMKqUouD0No09H7qf+5ZaIb 6 | 3yGpC5J9Bsf/ZbvD5xpgqbGEYkHj7Qh6Z/cPCSHA+ZpsUzDXVrLFXrdwwiK1FrIS 7 | rDI2RYsiP+e52XPC/acWC/7f+E54C62oMjYojaVaDn8gu06gyS1rXK2JITQ6CrKn 8 | b+PVSkjtPB4ku245u1qCKoblkNEZSkEmw8Csl+gw6ydGqOSQAoo8rsDte5zCMnPX 9 | 7XzL6EhRqpiVx7PCuQWnXhL7j9N214Pit7s7F8TeAA6yZR9oswW+h0dWO+XwocJ1 10 | rwkODXOngbCqO+GUxyuavIl2m0d2MP8n6Wa9RVqYetmPQzafKkR5hjiV4mgCFqNQ 11 | bHMTjI6udcR+h5pYoWKxN9/gJaWwyAAzck0AiMeGVrvKR3JKACqlTMzy/Y30obRF 12 | dddURoFf2wjKJvuTK9hHI7pwM5tlPEwu9bTCWNA6XXs2Bq1f6N2OAKhpKOcihNem 13 | aeGUPmygLPb66z9JO75yZXM+1yk1ScXaNHWZLmluVpEPk7maWULpSpxPAlaN3PmK 14 | 8lEihgfBBovampxZo8SvPEt+g5jGyPq9weNg8ic8476PuRVQdg7D8spVxl6whDlJ 15 | bcFojzgrX70t13jqZOtla4WK1vRnZAGplfoH0i5WvAVw+i5S/OVzsmNDtGFbAgMB 16 | AAEwDQYJKoZIhvcNAQELBQADggIBADUAjA/dH+b5UxDC5SL98w1hphw9PvD1cuGS 17 | sVnKPM236JoTiO3KVfm3NMBfSoBi1hPNkXzqr/R4xbyje4Kc4oYcdjGtpll3T5da 18 | wkx1+qumx6O2mEaOshxh76dfZfZne6SQphQKHw8PD10CfDb/NMnmdEbiOSENSqS4 19 | jGELuGviUl361oCBU45UEN7lfs7ANAhwSZyEO7deroyGdvsxfQUaqQrEQsG30jn3 20 | t0cCamYU6eK3bNR/yNXJrZFv3dzoquRY9H52YtVElRqdAIsNlnbxbqz0cm5xFKFt 21 | YTIrMSO1EvDTbB0PPwC5FJvONHhjwiWzgVXSnZrcs/05TsWWnSHH92S+wGCIBC+0 22 | 6fcSKnjdBn9ks5TrDX0TRY6N890KyDQWxPRhHYrMVpn833WY8y/SguxqiMgLFgMD 23 | WLy6yZzJloW7NgpLGAfMA0nMG1O92hfKmQw82Pyf3SVXGTDiXiEOXn0vN6bsPaV/ 24 | 3Ws2LJQECnVfHj3TsuxdtwcO+VGcFCarMOqlhE6IlQzfK8ykYdP6wCkVgXEtiVCR 25 | T1OWUWCFowoFpwBFLf1lA065qsAymddnkrUEOMiScZ/3OZhmd+FvgQ+O0iYuqpeI 26 | xauiQ68+Jb4KjVWnu5QBVq8n1vUJ5+gAzowNMN9G+1+A282Ox23T48dce22BTS6B 27 | 3Taaccm+ 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import json 16 | import zipfile 17 | from pathlib import Path 18 | from typing import Any, Dict, List, Optional, Tuple, TypeVar 19 | 20 | 21 | def parse_trace(path: Path) -> Tuple[Dict[str, bytes], List[Any]]: 22 | resources: Dict[str, bytes] = {} 23 | with zipfile.ZipFile(path, "r") as zip: 24 | for name in zip.namelist(): 25 | resources[name] = zip.read(name) 26 | action_map: Dict[str, Any] = {} 27 | events: List[Any] = [] 28 | for name in ["trace.trace", "trace.network"]: 29 | for line in resources[name].decode().splitlines(): 30 | if not line: 31 | continue 32 | event = json.loads(line) 33 | if event["type"] == "before": 34 | event["type"] = "action" 35 | action_map[event["callId"]] = event 36 | events.append(event) 37 | elif event["type"] == "input": 38 | pass 39 | elif event["type"] == "after": 40 | existing = action_map[event["callId"]] 41 | existing["error"] = event.get("error", None) 42 | else: 43 | events.append(event) 44 | return (resources, events) 45 | 46 | 47 | def get_trace_actions(events: List[Any]) -> List[str]: 48 | action_events = sorted( 49 | list( 50 | filter( 51 | lambda e: e["type"] == "action", 52 | events, 53 | ) 54 | ), 55 | key=lambda e: e["startTime"], 56 | ) 57 | return [e["apiName"] for e in action_events] 58 | 59 | 60 | TARGET_CLOSED_ERROR_MESSAGE = "Target page, context or browser has been closed" 61 | 62 | MustType = TypeVar("MustType") 63 | 64 | 65 | def must(value: Optional[MustType]) -> MustType: 66 | assert value 67 | return value 68 | 69 | 70 | def chromium_version_less_than(a: str, b: str) -> bool: 71 | left = list(map(int, a.split("."))) 72 | right = list(map(int, b.split("."))) 73 | for i in range(4): 74 | if left[i] > right[i]: 75 | return False 76 | if left[i] < right[i]: 77 | return True 78 | return False 79 | -------------------------------------------------------------------------------- /utils/docker/.gitignore: -------------------------------------------------------------------------------- 1 | oras/ 2 | -------------------------------------------------------------------------------- /utils/docker/Dockerfile.jammy: -------------------------------------------------------------------------------- 1 | FROM ubuntu:jammy 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | ARG TZ=America/Los_Angeles 5 | ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright/python:v%version%-jammy" 6 | 7 | ENV LANG=C.UTF-8 8 | ENV LC_ALL=C.UTF-8 9 | 10 | # === INSTALL Python === 11 | 12 | RUN apt-get update && \ 13 | # Install Python 14 | apt-get install -y python3 python3-distutils curl && \ 15 | update-alternatives --install /usr/bin/python python /usr/bin/python3 1 && \ 16 | curl -sSL https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \ 17 | python get-pip.py && \ 18 | rm get-pip.py && \ 19 | # Feature-parity with node.js base images. 20 | apt-get install -y --no-install-recommends git openssh-client gpg && \ 21 | # clean apt cache 22 | rm -rf /var/lib/apt/lists/* && \ 23 | # Create the pwuser 24 | adduser pwuser 25 | 26 | # === BAKE BROWSERS INTO IMAGE === 27 | 28 | ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright 29 | 30 | # 1. Add tip-of-tree Playwright package to install its browsers. 31 | # The package should be built beforehand from tip-of-tree Playwright. 32 | COPY ./dist/*-manylinux*.whl /tmp/ 33 | 34 | # 2. Bake in browsers & deps. 35 | # Browsers will be downloaded in `/ms-playwright`. 36 | # Note: make sure to set 777 to the registry so that any user can access 37 | # registry. 38 | RUN mkdir /ms-playwright && \ 39 | mkdir /ms-playwright-agent && \ 40 | cd /ms-playwright-agent && \ 41 | pip install virtualenv && \ 42 | virtualenv venv && \ 43 | . venv/bin/activate && \ 44 | # if its amd64 then install the manylinux1_x86_64 pip package 45 | if [ "$(uname -m)" = "x86_64" ]; then pip install /tmp/*manylinux1_x86_64*.whl; fi && \ 46 | # if its arm64 then install the manylinux1_aarch64 pip package 47 | if [ "$(uname -m)" = "aarch64" ]; then pip install /tmp/*manylinux_2_17_aarch64*.whl; fi && \ 48 | playwright mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \ 49 | playwright install --with-deps && rm -rf /var/lib/apt/lists/* && \ 50 | # Workaround for https://github.com/microsoft/playwright/issues/27313 51 | # While the gstreamer plugin load process can be in-process, it ended up throwing 52 | # an error that it can't have libsoup2 and libsoup3 in the same process because 53 | # libgstwebrtc is linked against libsoup2. So we just remove the plugin. 54 | if [ "$(uname -m)" = "aarch64" ]; then \ 55 | rm /usr/lib/aarch64-linux-gnu/gstreamer-1.0/libgstwebrtc.so; \ 56 | else \ 57 | rm /usr/lib/x86_64-linux-gnu/gstreamer-1.0/libgstwebrtc.so; \ 58 | fi && \ 59 | rm /tmp/*.whl && \ 60 | rm -rf /ms-playwright-agent && \ 61 | chmod -R 777 /ms-playwright 62 | -------------------------------------------------------------------------------- /utils/docker/Dockerfile.noble: -------------------------------------------------------------------------------- 1 | FROM ubuntu:noble 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | ARG TZ=America/Los_Angeles 5 | ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright/python:v%version%-noble" 6 | 7 | ENV LANG=C.UTF-8 8 | ENV LC_ALL=C.UTF-8 9 | 10 | # === INSTALL Python === 11 | 12 | RUN apt-get update && \ 13 | # Install Python 14 | apt-get install -y python3 curl && \ 15 | # Align with upstream Python image and don't be externally managed: 16 | # https://github.com/docker-library/python/issues/948 17 | rm /usr/lib/python3.12/EXTERNALLY-MANAGED && \ 18 | update-alternatives --install /usr/bin/python python /usr/bin/python3 1 && \ 19 | curl -sSL https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \ 20 | python get-pip.py && \ 21 | rm get-pip.py && \ 22 | # Feature-parity with node.js base images. 23 | apt-get install -y --no-install-recommends git openssh-client gpg && \ 24 | # clean apt cache 25 | rm -rf /var/lib/apt/lists/* && \ 26 | # Create the pwuser 27 | adduser pwuser 28 | 29 | # === BAKE BROWSERS INTO IMAGE === 30 | 31 | ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright 32 | 33 | # 1. Add tip-of-tree Playwright package to install its browsers. 34 | # The package should be built beforehand from tip-of-tree Playwright. 35 | COPY ./dist/*-manylinux*.whl /tmp/ 36 | 37 | # 2. Bake in browsers & deps. 38 | # Browsers will be downloaded in `/ms-playwright`. 39 | # Note: make sure to set 777 to the registry so that any user can access 40 | # registry. 41 | RUN mkdir /ms-playwright && \ 42 | mkdir /ms-playwright-agent && \ 43 | cd /ms-playwright-agent && \ 44 | pip install virtualenv && \ 45 | virtualenv venv && \ 46 | . venv/bin/activate && \ 47 | # if its amd64 then install the manylinux1_x86_64 pip package 48 | if [ "$(uname -m)" = "x86_64" ]; then pip install /tmp/*manylinux1_x86_64*.whl; fi && \ 49 | # if its arm64 then install the manylinux1_aarch64 pip package 50 | if [ "$(uname -m)" = "aarch64" ]; then pip install /tmp/*manylinux_2_17_aarch64*.whl; fi && \ 51 | playwright mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \ 52 | playwright install --with-deps && rm -rf /var/lib/apt/lists/* && \ 53 | rm /tmp/*.whl && \ 54 | rm -rf /ms-playwright-agent && \ 55 | chmod -R 777 /ms-playwright 56 | -------------------------------------------------------------------------------- /utils/docker/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set +x 4 | 5 | if [[ ($1 == '--help') || ($1 == '-h') || ($1 == '') || ($2 == '') ]]; then 6 | echo "usage: $(basename $0) {--arm64,--amd64} {jammy,noble} playwright:localbuild-noble" 7 | echo 8 | echo "Build Playwright docker image and tag it as 'playwright:localbuild-noble'." 9 | echo "Once image is built, you can run it with" 10 | echo "" 11 | echo " docker run --rm -it playwright:localbuild-noble /bin/bash" 12 | echo "" 13 | echo "NOTE: this requires on Playwright PIP dependencies to be installed" 14 | echo "" 15 | exit 0 16 | fi 17 | 18 | function cleanup() { 19 | rm -rf "dist/" 20 | } 21 | 22 | trap "cleanup; cd $(pwd -P)" EXIT 23 | cd "$(dirname "$0")" 24 | 25 | pushd ../../ 26 | for wheel in $(python setup.py --list-wheels); do 27 | PLAYWRIGHT_TARGET_WHEEL=$wheel python -m build --wheel 28 | done 29 | popd 30 | mkdir dist/ 31 | cp ../../dist/*-manylinux*.whl dist/ 32 | 33 | PLATFORM="" 34 | if [[ "$1" == "--arm64" ]]; then 35 | PLATFORM="linux/arm64"; 36 | elif [[ "$1" == "--amd64" ]]; then 37 | PLATFORM="linux/amd64" 38 | else 39 | echo "ERROR: unknown platform specifier - $1. Only --arm64 or --amd64 is supported" 40 | exit 1 41 | fi 42 | 43 | docker build --platform "${PLATFORM}" -t "$3" -f "Dockerfile.$2" . 44 | -------------------------------------------------------------------------------- /utils/linting/check_file_header.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) Microsoft Corporation. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import sys 17 | from pathlib import Path 18 | from typing import List 19 | 20 | LICENSE_HEADER = """ 21 | # Copyright (c) Microsoft Corporation. 22 | # 23 | # Licensed under the Apache License, Version 2.0 (the "License"); 24 | # you may not use this file except in compliance with the License. 25 | # You may obtain a copy of the License at 26 | # 27 | # http://www.apache.org/licenses/LICENSE-2.0 28 | # 29 | # Unless required by applicable law or agreed to in writing, software 30 | # distributed under the License is distributed on an "AS IS" BASIS, 31 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 32 | # See the License for the specific language governing permissions and 33 | # limitations under the License. 34 | """.strip() 35 | 36 | 37 | def file_has_license(file_path: str) -> None: 38 | path = Path(file_path) 39 | if not path.name.endswith(".py"): 40 | return 41 | if not path.exists(): 42 | raise Exception(f"File {file_path} does not exist") 43 | content = path.read_text(encoding="utf-8") 44 | if content.strip() == "": 45 | return 46 | if content.startswith("#!"): 47 | # flake8: noqa: E203 48 | content = content[content.find("\n") + 1 :] 49 | if not content.startswith(LICENSE_HEADER): 50 | raise Exception(f"File {file_path} does not have license header") 51 | 52 | 53 | def main() -> None: 54 | errors: List[str] = [] 55 | 56 | for file_path in sys.argv[1:]: 57 | try: 58 | file_has_license(file_path) 59 | except Exception as e: 60 | errors.append(str(e)) 61 | 62 | if errors: 63 | print("Missing license header in files:") 64 | for error in errors: 65 | print(" -", error) 66 | sys.exit(1) 67 | 68 | 69 | if __name__ == "__main__": 70 | main() 71 | --------------------------------------------------------------------------------